Skip to content
This repository was archived by the owner on Aug 1, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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