diff --git a/README.md b/README.md index a7e2094..eb69e5f 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,29 @@ Then add the following CORS configuration to the S3 bucket: ``` +## Uploads with custom uploader action + +You also can upload images via a own provided action. For example when you want to preprocess the images. For this you have to set the uploader_action_path in the initializer. When you have set the S3 config and the uploader_action_path, the uploader_action_path will win. + +```ruby +ActiveAdmin::Editor.configure do |config| + config.uploader_action_path = '/path/to/the/uploader' +end +``` + +__pseudocode of an uploader action within active admin__ + +```ruby +collection_action :upload_image, :method => :post do + img = ImageUploader.new(image: params[:file]) + img.upload + + # IMPORTANT the image url must be set as the headers location porperty + render json: {location: img.remote_url} , location: img.remote_url +end +``` + + ## Configuration You can configure the editor in the initializer installed with `rails g diff --git a/app/assets/javascripts/active_admin/editor/config.js.erb b/app/assets/javascripts/active_admin/editor/config.js.erb index 49a0014..f3b7792 100644 --- a/app/assets/javascripts/active_admin/editor/config.js.erb +++ b/app/assets/javascripts/active_admin/editor/config.js.erb @@ -8,6 +8,12 @@ config.stylesheets = <%= ActiveAdmin::Editor.configuration.stylesheets.map { |stylesheet| asset_path stylesheet }.to_json %> config.spinner = '<%= asset_path 'active_admin/editor/loader.gif' %>' - config.uploads_enabled = <%= ActiveAdmin::Editor.configuration.s3_configured? %> + config.uploads_enabled = <%= ActiveAdmin::Editor.configuration.uploads_enabled? %> config.parserRules = <%= ActiveAdmin::Editor.configuration.parser_rules.to_json %> + + <% if path = ActiveAdmin::Editor.configuration.uploader_action_path %> + config.uploader_action_path = '<%= path.to_s %>' + <% else %> + config.uploader_action_path = null + <% end %> })(window) diff --git a/app/assets/javascripts/active_admin/editor/editor.js b/app/assets/javascripts/active_admin/editor/editor.js index 11365b6..260a096 100644 --- a/app/assets/javascripts/active_admin/editor/editor.js +++ b/app/assets/javascripts/active_admin/editor/editor.js @@ -47,7 +47,7 @@ * Adds a file input attached to the supplied text input. And upload is * triggered if the source of the input is changed. * - * @input Text input to attach a file input to. + * @input Text input to attach a file input to. */ Editor.prototype._addUploader = function(input) { var $input = $(input) @@ -93,6 +93,63 @@ return this.__uploading } + /** + * Uploads a file to S3 or an custom action. + * + * @file The file to upload + * @callback A function to be called when the upload completes. + */ + Editor.prototype.upload = function(file, callback) { + if (config.uploader_action_path == null) { + return this.s3_upload(file, callback) + } else { + return this.action_upload(file, callback) + } + } + + /** + * Uploads a file to a confured action under config.uploader_action_path. + * When the upload is complete, calls callback with the location of the uploaded file. + * + * @file The file to upload + * @callback A function to be called when the upload completes. + */ + Editor.prototype.action_upload = function(file, callback) { + var _this = this + _this._uploading(true) + + var xhr = new XMLHttpRequest() + , fd = new FormData() + + fd.append('_method', 'POST') + fd.append($('meta[name="csrf-param"]').attr('content'), $('meta[name="csrf-token"]').attr('content')) + fd.append('file', file) + + xhr.upload.addEventListener('progress', function(e) { + _this.loaded = e.loaded + _this.total = e.total + _this.progress = e.loaded / e.total + }, false) + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) { return } + _this._uploading(false) + if (xhr.status == 200) { + callback(xhr.getResponseHeader('Location')) + } else { + alert('Failed to upload file. Have you implemented action "' + config.uploader_action_path + '" correctly?') + } + } + + action_url = window.location.protocol + '//' + window.location.host + config.uploader_action_path + xhr.open('POST', action_url, true) + xhr.send(fd) + + return xhr + } + + /** + /** * Uploads a file to S3. When the upload is complete, calls callback with the * location of the uploaded file. @@ -100,7 +157,7 @@ * @file The file to upload * @callback A function to be called when the upload completes. */ - Editor.prototype.upload = function(file, callback) { + Editor.prototype.s3_upload = function(file, callback) { var _this = this _this._uploading(true) diff --git a/lib/active_admin/editor/config.rb b/lib/active_admin/editor/config.rb index 1478296..60950de 100644 --- a/lib/active_admin/editor/config.rb +++ b/lib/active_admin/editor/config.rb @@ -29,6 +29,9 @@ class Configuration # wysiwyg stylesheets that get included in the backend and the frontend. attr_accessor :stylesheets + # action which should handle file upload + attr_accessor :uploader_action_path + def storage_dir @storage_dir ||= 'uploads' end @@ -47,9 +50,17 @@ def s3_configured? s3_bucket.present? end + def uploads_enabled? + s3_configured? or @uploader_action_path.present? + end + def parser_rules @parser_rules ||= PARSER_RULES.dup end + + def uploader_action_path=(action) + @uploader_action_path = (action.nil?) ? action : "/#{ action.to_s.gsub(/(^\/|\/$)/, '') }" + end end end end diff --git a/spec/javascripts/editor_spec.js b/spec/javascripts/editor_spec.js index c459b83..7510005 100644 --- a/spec/javascripts/editor_spec.js +++ b/spec/javascripts/editor_spec.js @@ -83,6 +83,28 @@ describe('Editor', function() { }) describe('.upload', function() { + it('calls s3_upload when uploader_action_path is not set', function() { + this.editor.s3_upload = sinon.stub() + this.editor.action_upload = sinon.stub() + this.config.s3_bucket = 'bucket' + this.config.uploader_action_path= null + xhr = this.editor.upload(sinon.stub(), function() {}) + expect(this.editor.s3_upload).to.have.been.called + expect(this.editor.action_upload).not.to.have.been.called + }) + + it('calls action_upload when uploader_action_path is set', function() { + this.editor.s3_upload = sinon.stub() + this.editor.action_upload = sinon.stub() + this.config.s3_bucket = 'bucket' + this.config.uploader_action_path= '/uploader/action' + xhr = this.editor.upload(sinon.stub(), function() {}) + expect(this.editor.s3_upload).not.to.have.been.called + expect(this.editor.action_upload).to.have.been.called + }) + }) + + describe('.s3_upload', function() { beforeEach(function() { this.xhr.prototype.upload = { addEventListener: sinon.stub() } }) @@ -91,13 +113,13 @@ describe('Editor', function() { this.xhr.prototype.open = sinon.stub() this.xhr.prototype.send = sinon.stub() this.config.s3_bucket = 'bucket' - xhr = this.editor.upload(sinon.stub(), function() {}) + xhr = this.editor.s3_upload(sinon.stub(), function() {}) expect(xhr.open).to.have.been.calledWith('POST', 'https://bucket.s3.amazonaws.com', true) }) it('sends the request', function() { this.xhr.prototype.send = sinon.stub() - xhr = this.editor.upload(sinon.stub(), function() {}) + xhr = this.editor.s3_upload(sinon.stub(), function() {}) expect(xhr.send).to.have.been.called }) @@ -106,7 +128,7 @@ describe('Editor', function() { this.xhr.prototype.open = sinon.stub() this.xhr.prototype.send = sinon.stub() this.config.s3_bucket = 'bucket' - xhr = this.editor.upload(sinon.stub(), function(location) { + xhr = this.editor.s3_upload(sinon.stub(), function(location) { expect(location).to.eq('foo') done() }) @@ -123,7 +145,7 @@ describe('Editor', function() { this.xhr.prototype.send = sinon.stub() this.config.s3_bucket = 'bucket' alert = sinon.stub() - xhr = this.editor.upload(sinon.stub(), function() {}) + xhr = this.editor.s3_upload(sinon.stub(), function() {}) xhr.readyState = 4 xhr.status = 403 xhr.onreadystatechange() @@ -146,7 +168,7 @@ describe('Editor', function() { this.config.storage_dir = 'uploads' this.config.aws_access_key_id = 'access key' - this.editor.upload(file, function() {}) + this.editor.s3_upload(file, function() {}) }) it('sets "key"', function() { @@ -178,4 +200,84 @@ describe('Editor', function() { }) }) }) + + describe('.action_upload', function() { + beforeEach(function() { + this.xhr.prototype.upload = { addEventListener: sinon.stub() } + }) + + it('opens the connection to the uploader action', function() { + this.xhr.prototype.open = sinon.stub() + this.xhr.prototype.send = sinon.stub() + this.config.uploader_action_path = '/path/to/action' + xhr = this.editor.action_upload(sinon.stub(), function() {}) + action_url = window.location.protocol + '//' + window.location.host + this.config.uploader_action_path + expect(xhr.open).to.have.been.calledWith('POST', action_url, true) + }) + + it('sends the request', function() { + this.xhr.prototype.send = sinon.stub() + xhr = this.editor.action_upload(sinon.stub(), function() {}) + expect(xhr.send).to.have.been.called + }) + + describe('when the upload succeeds', function() { + it('calls the callback with the location', function(done) { + this.xhr.prototype.open = sinon.stub() + this.xhr.prototype.send = sinon.stub() + this.config.uploader_action_path = '/path/to/action' + xhr = this.editor.action_upload(sinon.stub(), function(location) { + expect(location).to.eq('foo') + done() + }) + xhr.getResponseHeader = sinon.stub().returns('foo') + xhr.readyState = 4 + xhr.status = 200 + xhr.onreadystatechange() + }) + }) + + describe('when the upload fails', function() { + it('shows an alert', function() { + this.xhr.prototype.open = sinon.stub() + this.xhr.prototype.send = sinon.stub() + this.config.uploader_action_path = '/path/to/action' + alert = sinon.stub() + xhr = this.editor.action_upload(sinon.stub(), function() {}) + xhr.readyState = 4 + xhr.status = 403 + xhr.onreadystatechange() + expect(alert).to.have.been.calledWith('Failed to upload file. Have you implemented action "' + this.config.uploader_action_path + '" correctly?') + }) + }) + + describe('form data', function() { + beforeEach(function() { + file = this.file = { name: 'foobar', type: 'image/jpg' } + append = this.append = sinon.stub() + FormData = function() { return { append: append } } + + Date.now = function() { return { toString: function() { return '1234' } } } + + this.xhr.prototype.open = sinon.stub() + this.xhr.prototype.send = sinon.stub() + + this.config.uploader_action_path = '/path/to/action' + + this.editor.action_upload(file, function() {}) + }) + + it('sets "_method"', function() { + expect(this.append).to.have.been.calledWith('_method', 'POST') + }) + + it('sets "authenticy_token"', function() { + expect(this.append).to.have.been.calledWith('authenticity_token', 'aMmw0/cl9FYg9Xi/SLCcdR0PASH1QOJrlQNr9rJOQ4g=') + }) + + it('sets "file"', function() { + expect(this.append).to.have.been.calledWith('file', this.file) + }) + }) + }) }) diff --git a/spec/javascripts/templates/editor.jst.ejs b/spec/javascripts/templates/editor.jst.ejs index 58ac953..052c083 100644 --- a/spec/javascripts/templates/editor.jst.ejs +++ b/spec/javascripts/templates/editor.jst.ejs @@ -1,3 +1,6 @@ + + +