Skip to content
This repository has been archived by the owner on Aug 1, 2018. It is now read-only.

Fileupload custom action #41

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,29 @@ Then add the following CORS configuration to the S3 bucket:
</CORSConfiguration>
```

## 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
Expand Down
8 changes: 7 additions & 1 deletion app/assets/javascripts/active_admin/editor/config.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
61 changes: 59 additions & 2 deletions app/assets/javascripts/active_admin/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -93,14 +93,71 @@
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.
*
* @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)

Expand Down
11 changes: 11 additions & 0 deletions lib/active_admin/editor/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
112 changes: 107 additions & 5 deletions spec/javascripts/editor_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
})
Expand All @@ -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
})

Expand All @@ -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()
})
Expand All @@ -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()
Expand All @@ -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() {
Expand Down Expand Up @@ -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)
})
})
})
})
3 changes: 3 additions & 0 deletions spec/javascripts/templates/editor.jst.ejs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<meta content="aMmw0/cl9FYg9Xi/SLCcdR0PASH1QOJrlQNr9rJOQ4g=" name="csrf-token">
<meta content="authenticity_token" name="csrf-param">

<li class="html_editor input optional" data-policy="{&quot;document&quot;:&quot;policy document&quot;,&quot;signature&quot;:&quot;policy signature&quot;}" id="page_content_input">
<label class=" label" for="page_content">Content</label>
<div class="wrap">
Expand Down
21 changes: 19 additions & 2 deletions spec/lib/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
configuration.aws_access_secret = nil
configuration.s3_bucket = nil
configuration.storage_dir = 'uploads'
configuration.uploader_action_path = nil
end

context 'by default' do
its(:aws_access_key_id) { should be_nil }
its(:aws_access_secret) { should be_nil }
its(:s3_bucket) { should be_nil }
its(:storage_dir) { should eq 'uploads' }
its(:uploader_action_path){ should be_nil }
end

describe '.s3_configured?' do
subject { configuration.s3_configured? }

Expand All @@ -26,7 +28,7 @@

it { should be_false }
end

context 'when key, secret and bucket are set' do
before do
configuration.aws_access_key_id = 'foo'
Expand All @@ -51,4 +53,19 @@
expect(subject).to eq 'uploads'
end
end

describe ".uploader_action_path" do
subject { configuration.uploader_action_path }

it 'strips trailing slashes' do
configuration.uploader_action_path = '/action/'
expect(subject).to eq '/action'
end

it 'add leading slash' do
configuration.uploader_action_path= 'action'
expect(subject).to eq '/action'
end
end

end