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"
    }],
    isEmpty: function() {
        return !this.get('name') && this.get('chapters').isEmpty();
    },
    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()
        };
    }
});
CMS.Collections.TextbookSet = Backbone.Collection.extend({
    model: CMS.Models.Textbook,
    url: function() { return CMS.URL.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": "",
        "selectedFile": null,
        "uploading": false,
        "uploadedBytes": 0,
        "totalBytes": 0,
        "finished": false
    },
    validate: function(attrs, options) {
        if(attrs.selectedFile && attrs.selectedFile.type !== "application/pdf") {
            return gettext("Only PDF files can be uploaded. Please select a file ending in .pdf to upload.");
        }
    }
});

@@ -1159,32 +1159,869 @@
 ON()%0A        %7D;%0A
+    %7D,%0A    validate: function(attrs, options) %7B%0A        if (!attrs.name) %7B%0A            return %22name is required%22;%0A        %7D%0A        if (attrs.chapters.length === 0) %7B%0A            return %22at least one asset is required%22;%0A        %7D else if (attrs.chapters.length === 1) %7B%0A            // only asset_path is required: we don't need a name%0A            if (!attrs.chapters.first().get('asset_path')) %7B%0A                return %22at least one asset is required%22;%0A            %7D%0A        %7D else %7B%0A            // validate all chapters%0A            var allChaptersValid = true;%0A            attrs.chapters.each(function(chapter) %7B%0A                if(!chapter.isValid()) %7B%0A                    allChaptersValid = false;%0A                %7D%0A            %7D);%0A            if(!allChaptersValid) %7B%0A                return %22invalid chapter%22;%0A            %7D%0A        %7D%0A
     %7D%0A%7D);%0ACMS.Co
@@ -3068,24 +3068,346 @@
 %0A        %7D;%0A
+    %7D,%0A    validate: function(attrs, options) %7B%0A        if(!attrs.name && !attrs.asset_path) %7B%0A            return %22name and asset_path are both required%22;%0A        %7D else if(!attrs.name) %7B%0A            return %22name is required%22;%0A        %7D else if (!attrs.asset_path) %7B%0A            return %22asset_path is required%22;%0A        %7D%0A
     %7D%0A%7D);%0ACM

Add validation functions for PDF Textbook models