Skip to content

Commit

Permalink
Generalize file uploader.
Browse files Browse the repository at this point in the history
Previously the file upload dialog was PDF- and textbook-specific. The
changes are adding parameters to the FileUpload model for the file
type, and adding an onSuccess callback to the UploadDialog view. Also
moved upload-specific SASS into its own file.
  • Loading branch information
Peter Fogg committed Aug 15, 2013
1 parent f4fe6de commit e8f40bc
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 345 deletions.
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
'js/models/course.js',
'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.js',
'js/views/assets.js', 'js/utility.js'],
'output_filename': 'js/cms-application.js',
Expand Down
12 changes: 10 additions & 2 deletions cms/static/coffee/spec/views/textbook_spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ describe "CMS.Views.EditChapter", ->
@view.render().$(".action-upload").click()
ctorOptions = uploadSpies.constructor.mostRecentCall.args[0]
expect(ctorOptions.model.get('title')).toMatch(/abcde/)
expect(ctorOptions.chapter).toBe(@model)
expect(typeof ctorOptions.onSuccess).toBe('function')
expect(uploadSpies.show).toHaveBeenCalled()

it "saves content when opening upload dialog", ->
Expand All @@ -323,7 +323,15 @@ describe "CMS.Views.UploadDialog", ->

@model = new CMS.Models.FileUpload()
@chapter = new CMS.Models.Chapter()
@view = new CMS.Views.UploadDialog({model: @model, chapter: @chapter})
@view = new CMS.Views.UploadDialog(
model: @model
onSuccess: (response) =>
options = {}
if !@chapter.get('name')
options.name = response.displayname
options.asset_path = response.url
@chapter.set(options)
)
spyOn(@view, 'remove').andCallThrough()

# create mock file input, so that we aren't subject to browser restrictions
Expand Down
22 changes: 1 addition & 21 deletions cms/static/js/models/textbook.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,24 +155,4 @@ CMS.Collections.ChapterSet = Backbone.Collection.extend({
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}
};
}
}
});

23 changes: 23 additions & 0 deletions cms/static/js/models/uploads.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
CMS.Models.FileUpload = Backbone.Model.extend({
defaults: {
"title": "",
"message": "",
"selectedFile": null,
"uploading": false,
"uploadedBytes": 0,
"totalBytes": 0,
"finished": false,
"mimeType": "application/pdf",
"fileType": "PDF"
},
// 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 !== this.attributes.mimeType) {
return {
message: "Only " + this.attributes.fileType + " files can be uploaded. Please select a file ending in ." + this.attributes.fileType.toLowerCase() + " to upload.",
attributes: {selectedFile: true}
};
}
}
});
122 changes: 11 additions & 111 deletions cms/static/js/views/textbook.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,118 +245,18 @@ CMS.Views.EditChapter = Backbone.View.extend({
{name: section.escape('name')}),
message: "Files must be in PDF format."
});
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,
successMessageTimeout: 2000 // 2 seconds
},
initialize: function() {
this.template = _.template($("#upload-dialog-tpl").text());
this.listenTo(this.model, "change", this.render);
},
render: function() {
var isValid = this.model.isValid();
var selectedFile = this.model.get('selectedFile');
var oldInput = this.$("input[type=file]").get(0);
this.$el.html(this.template({
shown: this.options.shown,
url: CMS.URL.UPLOAD_ASSET,
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'),
finished: this.model.get('finished'),
error: this.model.validationError
}));
// Ideally, we'd like to tell the browser to pre-populate the
// <input type="file"> with the selectedFile if we have one -- but
// browser security prohibits that. So instead, we'll swap out the
// new input (that has no file selected) with the old input (that
// already has the selectedFile selected). However, we only want to do
// this if the selected file is valid: if it isn't, we want to render
// a blank input to prompt the user to upload a different (valid) file.
if (selectedFile && isValid) {
$(oldInput).removeClass("error");
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({
selectedFile: e.target.files[0] || null
});
},
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: {
// don't show the generic error notification; we're in a modal,
// and we're better off modifying it instead.
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,
finished: true
});
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);
}
var that = this;
this.removalTimeout = setTimeout(function() {
that.hide().remove();
}, this.options.successMessageTimeout);
},
error: function() {
this.model.set({
"uploading": false,
"uploadedBytes": 0,
"title": gettext("We're sorry, there was an error")
var view = new CMS.Views.UploadDialog({
model: msg,
onSuccess: function(response) {
var options = {};
if(!that.model.get('name')) {
options.name = response.displayname;
}
options.asset_path = response.url;
that.model.set(options);
},
});
$(".wrapper-view").after(view.show().el);
}
});
105 changes: 105 additions & 0 deletions cms/static/js/views/uploads.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
CMS.Views.UploadDialog = Backbone.View.extend({
options: {
shown: true,
successMessageTimeout: 2000 // 2 seconds
},
initialize: function() {
this.template = _.template($("#upload-dialog-tpl").text());
this.listenTo(this.model, "change", this.render);
},
render: function() {
var isValid = this.model.isValid();
var selectedFile = this.model.get('selectedFile');
var oldInput = this.$("input[type=file]").get(0);
this.$el.html(this.template({
shown: this.options.shown,
url: CMS.URL.UPLOAD_ASSET,
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'),
finished: this.model.get('finished'),
error: this.model.validationError
}));
// Ideally, we'd like to tell the browser to pre-populate the
// <input type="file"> with the selectedFile if we have one -- but
// browser security prohibits that. So instead, we'll swap out the
// new input (that has no file selected) with the old input (that
// already has the selectedFile selected). However, we only want to do
// this if the selected file is valid: if it isn't, we want to render
// a blank input to prompt the user to upload a different (valid) file.
if (selectedFile && isValid) {
$(oldInput).removeClass("error");
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({
selectedFile: e.target.files[0] || null
});
},
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) {
if(e && e.preventDefault) { e.preventDefault(); }
this.model.set('uploading', true);
this.$("form").ajaxSubmit({
success: _.bind(this.success, this),
error: _.bind(this.error, this),
uploadProgress: _.bind(this.progress, this),
data: {
// don't show the generic error notification; we're in a modal,
// and we're better off modifying it instead.
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,
finished: true
});
if(this.options.onSuccess) {
this.options.onSuccess(response, statusText, xhr, form);
}
var that = this;
this.removalTimeout = setTimeout(function() {
that.hide().remove();
}, this.options.successMessageTimeout);
},
error: function() {
this.model.set({
"uploading": false,
"uploadedBytes": 0,
"title": gettext("We're sorry, there was an error")
});
}
});
1 change: 1 addition & 0 deletions cms/static/sass/base-style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
@import 'views/users';
@import 'views/checklists';
@import 'views/textbooks';
@import 'views/uploads';

// temp - inherited
@import 'assets/content-types';
Expand Down
Loading

0 comments on commit e8f40bc

Please sign in to comment.