diff --git a/cms/envs/common.py b/cms/envs/common.py
index d91e2aeeba..86b3d3f1df 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -244,6 +244,7 @@ PIPELINE_JS = {
'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',
diff --git a/cms/static/coffee/spec/views/textbook_spec.coffee b/cms/static/coffee/spec/views/textbook_spec.coffee
index 981659abfa..5185c9fb47 100644
--- a/cms/static/coffee/spec/views/textbook_spec.coffee
+++ b/cms/static/coffee/spec/views/textbook_spec.coffee
@@ -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", ->
@@ -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
diff --git a/cms/static/js/models/textbook.js b/cms/static/js/models/textbook.js
index cdd86023dc..72c9dfb0be 100644
--- a/cms/static/js/models/textbook.js
+++ b/cms/static/js/models/textbook.js
@@ -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}
- };
- }
- }
-});
+
diff --git a/cms/static/js/models/uploads.js b/cms/static/js/models/uploads.js
new file mode 100644
index 0000000000..ba557306eb
--- /dev/null
+++ b/cms/static/js/models/uploads.js
@@ -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}
+ };
+ }
+ }
+});
diff --git a/cms/static/js/views/textbook.js b/cms/static/js/views/textbook.js
index f666143c0a..992ac5cd53 100644
--- a/cms/static/js/views/textbook.js
+++ b/cms/static/js/views/textbook.js
@@ -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});
+ var that = this;
+ 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);
}
});
-
-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
- // 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")
- });
- }
-});
diff --git a/cms/static/js/views/uploads.js b/cms/static/js/views/uploads.js
new file mode 100644
index 0000000000..9ce111e805
--- /dev/null
+++ b/cms/static/js/views/uploads.js
@@ -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
+ // 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")
+ });
+ }
+});
diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss
index 7011089527..757ffe02bd 100644
--- a/cms/static/sass/base-style.scss
+++ b/cms/static/sass/base-style.scss
@@ -59,6 +59,7 @@
@import 'views/users';
@import 'views/checklists';
@import 'views/textbooks';
+@import 'views/uploads';
// temp - inherited
@import 'assets/content-types';
diff --git a/cms/static/sass/views/_textbooks.scss b/cms/static/sass/views/_textbooks.scss
index b83d22414b..ac644b6ff7 100644
--- a/cms/static/sass/views/_textbooks.scss
+++ b/cms/static/sass/views/_textbooks.scss
@@ -370,213 +370,4 @@ body.course.textbooks {
.content-supplementary {
width: flex-grid(3, 12);
}
-
- // dialog
-.wrapper-dialog {
- @extend .ui-depth5;
- @include transition(all 0.05s ease-in-out);
- position: fixed;
- top: 0;
- background: $black-t2;
- width: 100%;
- height: 100%;
- text-align: center;
-
- &:before {
- content: '';
- display: inline-block;
- height: 100%;
- vertical-align: middle;
- margin-right: -0.25em; /* Adjusts for spacing */
- }
-
- .dialog {
- @include box-sizing(border-box);
- box-shadow: 0px 0px 7px $shadow-d1;
- border-radius: ($baseline/5);
- background-color: $gray-l4;
- display: inline-block;
- vertical-align: middle;
- width: $baseline*23;
- padding: 7px;
- text-align: left;
-
- .title {
- @extend .t-title5;
- margin-bottom: ($baseline/2);
- font-weight: 600;
- color: $black;
- }
-
- .message {
- @extend .t-copy-sub2;
- color: $gray;
- }
-
- .error {
- color: $white;
- }
-
- form {
- padding: 0;
-
- .form-content {
- box-shadow: 0 0 3px $shadow-d1;
- padding: ($baseline*1.5);
- background-color: $white;
- }
-
- input[type="file"] {
- @extend .t-copy-sub2;
- }
-
- .status-upload {
- height: 30px;
- margin-top: $baseline;
-
- .wrapper-progress {
- box-shadow: inset 0 0 3px $shadow-d1;
- display: block;
- border-radius: ($baseline*0.75);
- background-color: $gray-l5;
- padding: 1px 8px 2px 8px;
- height: 25px;
-
- progress {
- display: inline-block;
- vertical-align: middle;
- width: 100%;
- border: none;
- border-radius: ($baseline*0.75);
- background-color: $gray-l5;
-
- &::-webkit-progress-bar {
- background-color: transparent;
- border-radius: ($baseline*0.75);
- }
-
- &::-webkit-progress-value {
- background-color: $pink;
- border-radius: ($baseline*0.75);
- }
-
- &::-moz-progress-bar {
- background-color: $pink;
- border-radius: ($baseline*0.75);
- }
-
- }
-
- }
-
- .message-status {
- @include border-top-radius(2px);
- @include box-sizing(border-box);
- @include font-size(14);
- display: none;
- border-bottom: 2px solid $yellow;
- margin: 0 0 20px 0;
- padding: 10px 20px;
- font-weight: 500;
- background: $paleYellow;
-
- .text {
- display: inline-block;
- }
-
- &.error {
- border-color: $red-d2;
- background: $red-l1;
- color: $white;
- }
-
- &.confirm {
- border-color: $green-d2;
- background: $green-l1;
- color: $white;
- }
-
- &.is-shown {
- display: block;
- }
- }
- }
-
- .actions {
- padding: ($baseline*0.75) $baseline ($baseline/2) $baseline;
-
-
-
- .action-item {
- @extend .t-action4;
- display: inline-block;
- margin-right: ($baseline*0.75);
-
- &:last-child {
- margin-right: 0;
- }
- }
-
- .action-primary {
- @include blue-button();
- @include font-size(12); // needed due to bad button mixins for now
- border-color: $blue-d1;
- color: $white;
- }
-
- a {
- color: $blue;
-
- &:hover {
- color: $blue-s2;
- }
- }
-
- }
-
- }
-
- }
-
-}
-
-// ====================
-
-// js enabled
-.js {
-
- // dialog set-up
- .wrapper-dialog {
- visibility: hidden;
- pointer-events: none;
-
- .dialog {
- opacity: 0;
- }
- }
-
- // dialog showing/hiding
- &.dialog-is-shown {
-
- .wrapper-dialog {
- -webkit-filter: blur(2px) grayscale(25%);
- filter: blur(2px) grayscale(25%);
- }
-
- .wrapper-dialog.is-shown {
- visibility: visible;
- pointer-events: auto;
-
- .dialog {
- opacity: 1.0;
- }
- }
- }
-
-}
-
-
-
-
-
}
diff --git a/cms/static/sass/views/_uploads.scss b/cms/static/sass/views/_uploads.scss
new file mode 100644
index 0000000000..f564ac1c13
--- /dev/null
+++ b/cms/static/sass/views/_uploads.scss
@@ -0,0 +1,209 @@
+// studio - views - uploads
+// ========================
+
+body.course.file-upload-dialog {
+
+ // dialog
+ .wrapper-dialog {
+ @extend .ui-depth5;
+ @include transition(all 0.05s ease-in-out);
+ position: fixed;
+ top: 0;
+ background: $black-t2;
+ width: 100%;
+ height: 100%;
+ text-align: center;
+
+ &:before {
+ content: '';
+ display: inline-block;
+ height: 100%;
+ vertical-align: middle;
+ margin-right: -0.25em; /* Adjusts for spacing */
+ }
+
+ .dialog {
+ @include box-sizing(border-box);
+ box-shadow: 0px 0px 7px $shadow-d1;
+ border-radius: ($baseline/5);
+ background-color: $gray-l4;
+ display: inline-block;
+ vertical-align: middle;
+ width: $baseline*23;
+ padding: 7px;
+ text-align: left;
+
+ .title {
+ @extend .t-title5;
+ margin-bottom: ($baseline/2);
+ font-weight: 600;
+ color: $black;
+ }
+
+ .message {
+ @extend .t-copy-sub2;
+ color: $gray;
+ }
+
+ .error {
+ color: $white;
+ }
+
+ form {
+ padding: 0;
+
+ .form-content {
+ box-shadow: 0 0 3px $shadow-d1;
+ padding: ($baseline*1.5);
+ background-color: $white;
+ }
+
+ input[type="file"] {
+ @extend .t-copy-sub2;
+ }
+
+ .status-upload {
+ height: 30px;
+ margin-top: $baseline;
+
+ .wrapper-progress {
+ box-shadow: inset 0 0 3px $shadow-d1;
+ display: block;
+ border-radius: ($baseline*0.75);
+ background-color: $gray-l5;
+ padding: 1px 8px 2px 8px;
+ height: 25px;
+
+ progress {
+ display: inline-block;
+ vertical-align: middle;
+ width: 100%;
+ border: none;
+ border-radius: ($baseline*0.75);
+ background-color: $gray-l5;
+
+ &::-webkit-progress-bar {
+ background-color: transparent;
+ border-radius: ($baseline*0.75);
+ }
+
+ &::-webkit-progress-value {
+ background-color: $pink;
+ border-radius: ($baseline*0.75);
+ }
+
+ &::-moz-progress-bar {
+ background-color: $pink;
+ border-radius: ($baseline*0.75);
+ }
+
+ }
+
+ }
+
+ .message-status {
+ @include border-top-radius(2px);
+ @include box-sizing(border-box);
+ @include font-size(14);
+ display: none;
+ border-bottom: 2px solid $yellow;
+ margin: 0 0 20px 0;
+ padding: 10px 20px;
+ font-weight: 500;
+ background: $paleYellow;
+
+ .text {
+ display: inline-block;
+ }
+
+ &.error {
+ border-color: $red-d2;
+ background: $red-l1;
+ color: $white;
+ }
+
+ &.confirm {
+ border-color: $green-d2;
+ background: $green-l1;
+ color: $white;
+ }
+
+ &.is-shown {
+ display: block;
+ }
+ }
+ }
+
+ .actions {
+ padding: ($baseline*0.75) $baseline ($baseline/2) $baseline;
+
+
+
+ .action-item {
+ @extend .t-action4;
+ display: inline-block;
+ margin-right: ($baseline*0.75);
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ .action-primary {
+ @include blue-button();
+ @include font-size(12); // needed due to bad button mixins for now
+ border-color: $blue-d1;
+ color: $white;
+ }
+
+ a {
+ color: $blue;
+
+ &:hover {
+ color: $blue-s2;
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ // ====================
+
+ // js enabled
+ .js {
+
+ // dialog set-up
+ .wrapper-dialog {
+ visibility: hidden;
+ pointer-events: none;
+
+ .dialog {
+ opacity: 0;
+ }
+ }
+
+ // dialog showing/hiding
+ &.dialog-is-shown {
+
+ .wrapper-dialog {
+ -webkit-filter: blur(2px) grayscale(25%);
+ filter: blur(2px) grayscale(25%);
+ }
+
+ .wrapper-dialog.is-shown {
+ visibility: visible;
+ pointer-events: auto;
+
+ .dialog {
+ opacity: 1.0;
+ }
+ }
+ }
+
+ }
+}
diff --git a/cms/templates/textbooks.html b/cms/templates/textbooks.html
index 28349b5436..5b79b7f265 100644
--- a/cms/templates/textbooks.html
+++ b/cms/templates/textbooks.html
@@ -4,7 +4,7 @@
<%! from django.utils.translation import ugettext as _ %>
<%block name="title">${_("Textbooks")}%block>
-<%block name="bodyclass">is-signedin course textbooks%block>
+<%block name="bodyclass">is-signedin course textbooks file-upload-dialog%block>
<%block name="header_extras">
% for template_name in ["edit-textbook", "show-textbook", "edit-chapter", "no-textbooks", "upload-dialog"]: