How to display embed video with ActionText - ruby-on-rails

I am trying to display embedded videos with ActionText on Rails 6, both in the WYSIWYG Trix, and in the rendered content. But the ActionText renderer filters all raw html code and forces me to use JS to display the iframes in the rendered content, which doesnt work in Trix.
I followed the instructions given here by one of Basecamp's dev : https://github.com/rails/actiontext/issues/37#issuecomment-451627370. Step 1 through 3 work, but when ActionText renders my partial it filters the iframe.
The form creating the WYSIYWG
= form_for(article, url: url, method: method) do |a|
= a.label :content
= a.rich_text_area :content, data: { controller: "articles", target: "articles.field", embeds_path: editorial_publication_embeds_path(#publication, format: :json) }
= a.submit submit_text, class:"btn full"
The Stimulus controller adding the embed functionality (in dire need of a refactor)
import { Controller } from "stimulus";
import Trix from "trix";
$.ajaxSetup({
headers: {
"X-CSRF-Token": $('meta[name="csrf-token"]').attr("content"),
},
});
export default class extends Controller {
static targets = ["field"];
connect() {
this.editor = this.fieldTarget.editor;
const buttonHTML =
'<button type="button" class="trix-button" data-trix-attribute="embed" data-trix-action="embed" title="Embed" tabindex="-1">Media</button>';
const buttonGroup = this.fieldTarget.toolbarElement.querySelector(
".trix-button-group--block-tools"
);
const dialogHml = `<div class="trix-dialog trix-dialog--link" data-trix-dialog="embed" data-trix-dialog-attribute="embed">
<div class="trix-dialog__link-fields">
<input type="text" name="embed" class="trix-input trix-input--dialog" placeholder="Paste your video or sound url" aria-label="embed code" required="" data-trix-input="" disabled="disabled">
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" data-trix-custom="add-embed" value="Add">
</div>
</div>
</div>`;
const dialogGroup = this.fieldTarget.toolbarElement.querySelector(
".trix-dialogs"
);
buttonGroup.insertAdjacentHTML("beforeend", buttonHTML);
dialogGroup.insertAdjacentHTML("beforeend", dialogHml);
document
.querySelector('[data-trix-action="embed"]')
.addEventListener("click", event => {
const dialog = document.querySelector('[data-trix-dialog="embed"]');
const embedInput = document.querySelector('[name="embed"]');
if (event.target.classList.contains("trix-active")) {
event.target.classList.remove("trix-active");
dialog.classList.remove("trix-active");
delete dialog.dataset.trixActive;
embedInput.setAttribute("disabled", "disabled");
} else {
event.target.classList.add("trix-active");
dialog.classList.add("trix-active");
dialog.dataset.trixActive = "";
embedInput.removeAttribute("disabled");
embedInput.focus();
}
});
document
.querySelector('[data-trix-custom="add-embed"]')
.addEventListener("click", event => {
const content = document.querySelector('[name="embed"]').value;
if (content) {
$.ajax({
method: "POST",
url: document.querySelector("[data-embeds-path]").dataset
.embedsPath,
data: {
embed: {
content,
},
},
success: ({ content, sgid }) => {
const attachment = new Trix.Attachment({
content,
sgid,
});
this.editor.insertAttachment(attachment);
this.editor.insertLineBreak();
},
});
}
});
}
}
The Embed model
class Embed < ApplicationRecord
include ActionText::Attachable
validates :content, presence: true
after_validation :fetch_oembed_data
def to_partial_path
"editorial/embeds/embed"
end
def fetch_oembed_data
url =
case content
when /youtube/
"https://www.youtube.com/oembed?url=#{content}&format=json"
when /soundcloud/
"https://soundcloud.com/oembed?url=#{content}&format=json"
when /twitter/
"https://publish.twitter.com/oembed?url=#{content}"
end
res = RestClient.get url
json = JSON.parse(res.body, object_class: OpenStruct)
self.height = json.height
self.author_url = json.author_url
self.thumbnail_url = json.thumbnail_url
self.width = json.width
self.author_name = json.author_name
self.thumbnail_height = json.thumbnail_height
self.title = json.title
self.version = json.version
self.provider_url = json.provider_url
self.thumbnail_width = json.thumbnail_width
self.embed_type = json.type
self.provider_name = json.provider_name
self.html = json.html
end
end
The controller creating the Embed
def create
#embed = Embed.create!(params.require(:embed).permit(:content))
respond_to do |format|
format.json
end
end
The jbuilder view responding to the ajax call to create the Embed
json.extract! #embed, :content
json.sgid #embed.attachable_sgid
json.content render(partial: "editorial/embeds/embed", locals: { embed: #embed }, formats: [:html])
The Embed HTML partial (slim)
.youtube-embed.embed
.content
= image_tag(embed.thumbnail_url) if embed.thumbnail_url.present?
p = "Embed from #{embed.provider_name} (#{embed.content})"
p.embed-html = embed.html
And finally the JS code displaying the iframes when the Article's content with Embeds inside is displayed
$(document).ready(() => {
$(".embed").each(function(i, embed) {
const $embed = $(embed);
const p = $embed
.find(".content")
.replaceWith($embed.find(".embed-html").text());
});
});
If I change the Embed partial to
== embed.html
It displays properly in the WYSIWYG but not in the rendered view.

You need to add iframe to allowed_tags, add the following code in application.rb:
config.to_prepare do
ActionText::ContentHelper.allowed_tags << "iframe"
end

It looks like you need to whitelist the script that generates the iframe.
A quick test you can do is on the show page add the relevant JS for the content providers (I was testing Instagram attachments, so added <script async src="//www.instagram.com/embed.js"></script>).
It would be unwise to whitelist all <script> tags in ActionText views, but you can manage the script loading yourself.

I couldn't test your complex example, but if you want to put ActionText's HTML, it would be helpful.
Please try this:
<%= raw your_action_text_object.to_plain_text %>

Related

Value of ActionText body is always " "(empty string)

I am using Rails 6 API and React. I'm trying to build a Rich Text Editor with ActionText. When I send the RTE content from the Trix editor on the front end, it just doesn't set the ActionText body to the body I sent through with Axios.
I am sure that the body has come correctly to the controller because I used byebug and printed out the param value.
For example, it looked like this: <div><!--block-->test</div>
But whenever I try to view what it actually is by running announcement.details.to_s it returns " " for some reason.
I set the details field like this: has_rich_text :details in the Announcement model.
My controller which handles this looks like this:
module V1
class AnnouncementsController < ApplicationController
def create
announcement = Announcement.new(announcement_params)
announcement.author = #current_user
authorize announcement
if announcement.valid? && announcement.save
render json: { message: "Announcement successfully created! You can view it here." }, status: 201
else
render json: { messages: announcement.errors.full_messages }, status: 400
end
end
private
def announcement_params
params.require(:announcement).permit(:title, :details)
end
end
end
If it helps in any way, this is the React code:
const RTE = (props) => {
let trixInput = React.createRef()
useEffect(() => {
trixInput.current.addEventListener("trix-change", event => {
console.log("fired")
props.onChange(event.target.innerHTML)
})
}, [])
return (
<div>
<input
type="hidden"
id="trix"
value={props.value}
/>
<trix-editor
input="trix"
data-direct-upload-url={`${bURL}/rails/active_storage/direct_uploads`}
data-blob-url-template={`${bURL}/rails/active_storage/blobs/:signed_id/*filename`}
ref={trixInput}
className="trix-content"
></trix-editor>
</div>
);
}
And then I just normally pass it with Axios:
axios.post(`${bURL}/v1/announcements/create`, {
"announcement": {
"title": title,
"details": value
}
}, {
headers: {
'Authorization': `token goes here`
}
}).then(res => {
// success
}).catch(err => {
// error
})
If you need any more code snippets or information please comment.

How to trigger the file upload on the client side and not on form submission?

I have a working version of the active-storage example using s3 found here:
https://edgeguides.rubyonrails.org/active_storage_overview.html
Now I want to be able to perform the file upload not when I finishing filling the form but immediately after the user selects a file to upload.
Actually in my case I have a wysiwyg editor that has a on drop event that fires
var myCodeMirror = CodeMirror.fromTextArea(post_body, {
lineNumbers: true,
dragDrop: true
});
myCodeMirror.on('drop', function(data, e) {
var file;
var files;
// Check if files were dropped
files = e.dataTransfer.files;
if (files.length > 0) {
e.preventDefault();
e.stopPropagation();
file = files[0];
console.log('File: ' + file.name);
console.log('File: ' + file.type);
return false;
}
});
So is there, since the file drop triggers this event, for me to then send this to active-storage somehow so it will start uploading the file to S3 right away?
Triggering uploads from the client-side
Active Storage exposes the DirectUpload JavaScript class which you can use to trigger a file upload directly from the client-side.
You can leverage this for integrations with third-party plugins (e.g. Uppy, Dropzone) or with your own custom JS code.
Using DirectUpload
The first thing you need to do is make sure that AWS S3 is set up to handle direct uploads. This requires ensuring your CORS configuration is set up properly.
Next, you simply instantiate an instance of the DirectUpload class, passing it the file to upload and the upload URL.
import { DirectUpload } from "activestorage"
// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const input = document.querySelector('input[type=file]')
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
// handle errors OR persist to the model using 'blob.signed_id'
})
See full documentation here:
https://edgeguides.rubyonrails.org/active_storage_overview.html#integrating-with-libraries-or-frameworks
The DirectUpload#create method initiates the upload to S3 and returns with an error or the uploaded file blob.
Assuming there are no errors, the last step is to persist the uploaded file to the model. You can do this using blob.signed_id and putting it into a hidden field somewhere on the page OR with an AJAX request to update your model.
Uploading a file on drop
In the case above, to start the direct upload on the drop simply put the code above into the drop handler.
Something like this:
myCodeMirror.on('drop', function(data, e) {
// Get the file
var file = e.dataTransfer.files[0];
// You need a file input somewhere on the page...
const input = document.querySelector('input[type=file]')
const url = input.dataset.directUploadUrl
// Instantiate the DirectUploader object
const upload = new DirectUpload(file, url)
// Upload the file
upload.create((error, blob) => { ... })
});
Using the asset pipeline
If you are just using the asset pipeline and not using a JavaScript bundler tool, then you create instances of the DirectUpload class like this
const upload = new ActiveStorage.DirectUpload(file, url)
The main problem of the topic is - you cannot Import DataUpload in java script section of the form. But we can create object ImmediateUploader as follow:
Global Java script part
upload/uploader.js
import { DirectUpload } from "#rails/activestorage"
export default class Uploader {
constructor(file, url) {
this.file = file
this.url = url
this.directUpload = new DirectUpload(this.file, this.url, this)
}
upload() {
return new Promise((resolve, reject) => {
this.directUpload.create((error, blob) => {
if (error) {
// Handle the error
reject(error)
} else {
// Add an appropriately-named hidden input to the form
// with a value of blob.signed_id
resolve(blob)
}
})
})
}
}
upload/index.js
import Uploader from './uploader.js'
export default {
upload (file, url) {
const uploader = new Uploader(file, url)
return uploader.upload()
}
}
application.js
window.ImmediateUploader = require('./upload');
Form part
Now we can use ImmediateUploader to upload selected files directly to active storage and update images after load without commit:
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<div class="row">
<img id="avatar" class="centered-and-cropped" width="100" height="100" style="border-radius:50%" src="<%= url_for(user.photo) %>">
<button type="button" class="btn" onclick="event.preventDefault(); document.getElementById('user_photo').click()">Change avatar</button>
</div>
<%= f.file_field :photo, direct_upload: true, class: "hiddenfile" %>
</div>
<div class="form-actions">
<%= f.button :submit, t(".update"), class: 'btn btn-primary' %>
</div>
<% end %>
<% content_for :js do %>
<script>
const input = document.querySelector('input[type=file]')
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// clear uploaded files from the input
input.value = null
})
const uploadFile = (file) => {
// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const url = input.dataset.directUploadUrl;
ImmediateUploader.default.upload (file, url)
.then(blob => {
// get blob.signed_id and add it to form values to submit form
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
// Update new avatar Immediately
document.getElementById('avatar').src = '/rails/active_storage/blobs/' + blob.signed_id + '/' + blob.filename;
// Update photo in Database
axios.post('/users/photo', { 'photo': blob.signed_id }).then(response => {});
});
}</script>
<% end %>
Controller:
class RegistrationController < Devise::RegistrationsController
def update
super
#user = current_user
#user.avatar = url_for(#user.photo.variant(resize_to_limit: [300, 300]).processed) if #user.photo.attached?
#user.save
end
def updatephoto
#photo = params[:photo]
#user = current_user
#user.photo = #photo
#user.save
#user = current_user
#user.avatar = url_for(#user.photo.variant(resize_to_limit: [300, 300]).processed) if #user.photo.attached?
#user.save
end
end

Filter select fields based on previous select fields values

I'm new at RoR and I'm having a trouble in my app. The problem consists on filter a select field named "Solution", based on the others select fields above it.
Now, what the app do is to retrieve all information from BD about Area, Region, Associated, Solution and populate the select fields with these data. But the user wants that, when an area, a region and an associated is selected by the user, only the solutions about that associated in that region on that area should be shown.
Edit:
I'm almost there! I've made many changes in my app. The select fields are populated by controller action new and the function "populate_selects", which is called by the parameter before_action :popula_selects, only: [:new, :edit]. A new function was created in order to be called by AJAX and upgrade the "Solution" field:
Atendments_Controller < ApplicationController
before_action :populate_selects, only: [:new, :edit]
def new
#atend = atendment.new
end
def update_solution #AJAX
#solutions = atendment.joins(:solution).where("atendment_area_id = ? and atendment_region_id = ? and atendment_assoc_id = ?", params[:atendment_area_id], params[:atendment_region_id], params[:atendment_assoc_id])
respond_to do |format|
format.js
end
end
private
def populate_selects
#atendment_area = atendmentArea.where(status: true, user_id: current_user.id)
#atendment_region = atendmentRegion.where(status: true, user_id: current_user.id)
#atendment_assoc = atendmentRegionAssoc.where(status: true, assoc_id: current_user.entidade_id).where(atendment_region_id: #atendment_region.map(&:atendment_region_id))
#solutions = atendment.joins(:solution).where("atendment_area_id = ? and atendment_region_id = ? and atendment_assoc_id = ?", params[:atendment_area_id], params[:atendment_region_id], params[:atendment_region_assoc_id])
end
end
Below, the _form.html.erb code from view:
<div class="atendment-form">
<%= form_for :atendment, url: {action: "new"}, html: {method: "get"} do |f| %>
<div class="col-xs-6">
<%= f.select :atendment_area_id, options_for_select(#atendment_area.collect { |c| [ c.atendment_area.name, c.id ] }, 1), {:prompt=>"Área"}, { :class => 'form-control', :required => true, id: 'atendment_atendment_area_id' } %>
</div>
<div class="col-xs-6">
<%= f.select :atendment_region_id, options_for_select(#atendment_region.collect { |c| [ c.atendment_region.name, c.id ] }, 1), {:prompt=>"Região"}, { :class => 'form-control', :required => true, id: 'atendment_atendment_region_id' } %>
</div>
</div>
</div>
<div class="field">
<%= f.select :atendment_assoc_id, options_for_select(#atendment_assoc.collect { |c| [ c.atendment_region.name, c.id ] }, 1), {:prompt=>"Associado"}, { :class => 'form-control', :required => true, id: 'atendment_atendment_assoc_id' } %>
</div>
<div class="field">
<%= f.select :solution_id, options_for_select(#solutions.collect { |solution| [solution.name, solution.id] }, 0), {:prompt=>"Solução"}, { :class => 'form-control', :required => true, id: 'atendment_solution_id' } %>
</div>
</div>
Route to the new function:
resources :atendments do
collection do
get :update_solution
end
end
AJAX function which calls the "update_solution" and reset solution field's value (app/assets/javascript/atendment.js.coffee):
show_solutions = ->
$.ajax 'update_solution',
type: 'GET'
dataType: 'script'
data: {
atendment_area_id: $("#atendment_atendment_area_id").val()
atendment_region_id: $("#atendment_atendment_region_id").val()
atendment_assoc_id: $("#atendment_atendment_assoc_id").val()
}
error: (jqXHR, textStatus, errorThrown) ->
console.log("AJAX Error: #{textStatus}")
success: (data, textStatus, jqXHR) ->
console.log("OK!")
$(document).ready ->
$('#atendment_atendment_assoc_id').on 'change', ->
show_solutions()
So, I've created a .coffee file to render the partial that will return a new value to the "solution" field "option" tag
(app/views/atendment/update_solution.coffee):
$("#atendment_solution_id").empty()
.append("<%= escape_javascript(render :partial => 'solution') %>")
And, the last but not least, the partial containing the html code for the "option" tag mentioned above (app/views/atendments/_solution.html.erb):
<option value="<%= solution.id %>" selected="selected"><%= solution.nome %></option>
For any reason, the AJAX function doesn't print nothing on console (nor error neither success), but it calls the update_solution.coffee file. The point is, it doesn't update the option value due an error (500 internal server error). I don't know what am I doing wrong. If anybody could help me, I appreciate it.
I would do this with JS, can think any other way.
A function called by onchange that change the display attribute from each field that you need to hide or show.
I solved this with the following code:
assets/js/atendments.js
I changed the code because the last one had many bugs.
function getAssociated(){
var aau_id = $("#atendment_area_user_id").val()
var aru_id = $("#atendment_region_user_id").val();
$.getJSON("/controllers/atendments_controller/getAssociated/"+aru_id,
function ( callback ) {
if (callback != "error"){
var assoc = document.getElementById("atendment_region_associated_id");
while (assoc.firstChild) {
assoc.removeChild(assoc.firstChild);
}
var i = Object.keys(callback).length -1;
$("#atendment_region_associated_id").append("<option value=''>Associated</option>");
while (i >= 0) {
$("#atendment_region_associated_id").append("<option value='"+callback[Object.keys(callback)[i]]+"'>"+Object.keys(callback)[i]+"</option>");
i--;
}
}
});
get_solution_type();
}
function get_solution_type() {
var ara_id = $("#atendment_region_associated_id").val();
$.getJSON("/controllers/atendments_controller/getSolution/"+ara_id,
function ( callback ) {
if (callback != "error"){
var sol = document.getElementById("atendment_solution_id");
while (sol.firstChild) {
sol.removeChild(sol.firstChild);
}
var i = Object.keys(callback).length-1;
while (i >= 0) {
$("#atendment_solution_id").append("<option value='"+callback[Object.keys(callback)[i]]+"'>"+Object.keys(callback)[i]+"</option>");
i--;
}
}
});
var aau_id = $("#atendment_area_user_id").val();
$.getJSON("/controllers/atendments_controller/getType/"+aau_id,
function ( callback ) {
if (callback != "erro"){
var type = document.getElementById("atendment_type_id");
while (type.firstChild) {
type.removeChild(type.firstChild);
}
var i = 0;
while (i < (Object.keys(callback).length)) {
$("#atendment_type_id").append("<option value='"+callback[Object.keys(callback)[i]]+"'>"+Object.keys(callback)[i]+"</option>");
i++;
}
}
});
}
The $.getJSON performs ajax request to the controller that responds with JSON and update the select fields option tags.
controllers/atendments_controller
I just retrieve the data from DB and return as JSON
def getAssociated
aru_id = params[:atendment_region_user_id]
aras = AtendmentRegionAssociated.where("SQL here")
if aras.present?
render :json => aras.to_json
else
render :json => "error".to_json
end
end
def getSolution
ara_id = params[:atendment_region_associated_id]
sol = Solution.where("SQL here")
if sol.present?
render :json => sol.to_json
else
render :json => "error".to_json
end
end
def getType
aau_id = params[:atendment_area_user_id]
type = AtendmentType.where("SQL here")
if type.present?
render :json => type.to_json
else
render :json => "error".to_json
end
end
Update the routes and put the javascript functions in select fields onchange property. Now everything is working fine :D

Rails + React js + react-rails gem submit form with single component

I am trying to submit form in react js with Rails.
I am new to React js and it is my first app
I am getting error No route matches [POST] "/"
Using single component(jsx) to submit the form.I am getting routing error.
Following is my code
EDIT
I changed the route and now I got error "InvalidAuthenticityToken in ItemsController#create"
How can i raise or alert variable in in jsx file ?
I add following route in route.rb
resources :items
root :to => redirect("/items")
ItemsController
def index
#presenter = { :action => items_path,
:csrf_token => request_forgery_protection_token,
:csrf_param => form_authenticity_token
}
end
def create
#item = Item.new(item_params)
#item.save
end
private
def item_params
params.require(:item).permit(:name, :price)
end
Index.html.erb
<%= react_component('Form1', {:presenter => #presenter.to_json}, {:presenter => true})%>
Form1.js.jsx
var Form1 = React.createClass({
handeSubmit: function( e ){
e.preventDefault();
// var form = e.target;
// var name = form.querySelector('[name="item[name]"]').value;
// var price = form.queySelector('[name="item[price]"]').value;
var name = this.refs.name.getDOMNode().value.trim();
var price = this.refs.price.getDOMNode().value.trim();
if(!name || !price)
{
return false;
}
var formData = $( this.refs.form.getDOMNode() ).serialize();
var action = this.props.presenter.action
// alert({formData});
$.ajax({
data: formData,
url: action,
type: "POST",
dataType: "json",
});
},
render: function(){
return (
<form ref="form" className="" action={ this.props.presenter.action } acceptCharset="UTF-8" method="post" onSubmit={this.handleSubmit} >
<input type="hidden" name={ this.props.presenter.csrf_param } value={ this.props.presenter.csrf_token } />
<input ref="name" name="item[name]" /><br/>
<input ref="price" name="item[price]" /><br/>
<button type="submit"> Submit</button>
</form>
)
}
});
It looks like, for AJAX requests, you should send the CSRF token as a header, not as a form field.
(Docs: http://api.rubyonrails.org/classes/ActionView/Helpers/CsrfHelper.html#method-i-csrf_meta_tags)
Here's how you could add that header in your case:
var csrfToken = this.props.presenter.csrf_token;
$.ajax({
data: formData,
url: action,
type: "POST",
dataType: "json",
// Before sending, add the CSRF header:
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
},
});
Does that work for you?
By the way, one way I work around this is by using react_component for form fields, but using Rails' form_for to make the actual <form> tag. For example,
<%= form_for #item, remote: true do |f| %>
<!-- Rails will add CSRF token -->
<%= react_component("ItemFormFields", item: #item) %>
<% end %>

Rails direct upload to Amazon S3

I'm looking to add functionality to my Rails app to upload files directly to Amazon S3. From my research the general consensus seems to be to use the s3-swf-upload-plugin. I've setup a sample app using that gem but I can't get it to play nice with only allowing the selection of a single file. I'd also like to create a record post upload and use paperclip to create a thumbnail for which I can find little guidance.
So my questions are:
(1) am I on the right track using that gem or should I be taking another appraoch?
(2) are there any samples out there that I could use for reference?
Any assistance would be much appreciated.
Chris
Try a new Gem called CarrierWaveDirect it allows you to upload files direct to S3 using a html form and easily move the image processing into a background process
Not sure about whether you can modify it easily to only upload one file at a time, but this gem works very well for me. It is based on one of Ryan Bates' Railscast:
https://github.com/waynehoover/s3_direct_upload
Try looking into carrierwave https://github.com/jnicklas/carrierwave (supports s3)
Multi file uploads with carrierwave and uploadify http://blog.assimov.net/post/4306595758/multi-file-upload-with-uploadify-and-carrierwave-on
If you are using Rails 3, please check out my sample projects:
Sample project using Rails 3, Flash and MooTools-based FancyUploader to upload directly to S3: https://github.com/iwasrobbed/Rails3-S3-Uploader-FancyUploader
Sample project using Rails 3, Flash/Silverlight/GoogleGears/BrowserPlus and jQuery-based Plupload to upload directly to S3: https://github.com/iwasrobbed/Rails3-S3-Uploader-Plupload
By the way, you can do post-processing with Paperclip using something like this blog post describes:
http://www.railstoolkit.com/posts/fancyupload-amazon-s3-uploader-with-paperclip
I have adapted Heroku's direct to S3 upload solution in Rails (which uses jQuery-File-Upload and the aws-sdk gem) so uploads to S3 can be made remotely using ajax. I hope this is useful:
posts_controller.rb
before_action :set_s3_direct_post, only: [:index, :create]
before_action :delete_picture_from_s3, only: [:destroy]
class PostsController < ApplicationController
def index
.
.
end
def create
#post = #user.posts.build(post_params)
if #post.save
format.html
format.js
end
end
def destroy
Post.find(params[:id]).destroy
end
private
def set_s3_direct_post
return S3_BUCKET.presigned_post(key: "uploads/#{SecureRandom.uuid}/${filename}", success_action_status: '201', acl: 'public-read')
end
def delete_picture_from_s3
key = params[:picture_url].split('amazonaws.com/')[1]
S3_BUCKET.object(key).delete
return true
rescue => e
# If anyone knows a good way to deal with a defunct file sitting in the bucket, please speak up.
return true
end
def post_params
params.require(:post).permit(:content, :picture_url)
end
end
posts.html.erb
<div class="info" data-url="<%= #s3_direct_post.url %>"
data-formdata="<%= (#s3_direct_post.fields.to_json) %>"
data-host="<%= URI.parse(#s3_direct_post.url).host %>">
</div>
The form
<%= form_for(:post, url: :posts, method: :post,
html: { class: "post_form", id: "post_form-#{post.id}" }
) do |f| %>
<%= f.text_area :content, id: "postfield-#{post.id}", class: "postText" %>
<%= f.button( :submit, name: "Post", title: "Post" ) do %>
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<% end %>
<span class="postuploadbutton" id="postUp-<%= post.id %>" title="Add file" >
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span>
</span>
<span title="Cancel file" class="noticecancelupload" id="postCancel-<%= post.id %>" >
<span class="glyphicon glyphicon-remove-circle" aria-hidden="true"></span>
</span>
<%= f.file_field :picture_url, accept: 'image/jpeg,image/gif,image/png',
class: "notice_file_field", id: "postFile-#{post.id}" %>
<% end %>
_post.html.erb
<%= button_to post_path(
params: {
id: post.id,
picture_url: post.picture_url
}
),
class: 'btn btn-default btn-xs blurme',
data: { confirm: "Delete post: are you sure?" },
method: :delete do %>
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
<% end %>
Javascript in each _post.html.erb
$(document).off('click',"#postUp-<%= post.id %>");
$(document).on('click', '#postUp-<%= post.id %>', function(e) {
prepareUpload("#post_form-<%= post.id %>");
$('#postFile-<%= post.id %>').trigger("click");
});
$(document).off('click',"#postCancel-<%= post.id %>");
$(document).on('click', '#postCancel-<%= post.id %>', function(e) {
$(".appendedInput").remove(); // $('#postFile-<% post.id %>').val(""); doesn't work for me
$('.progBar').css('background','white').text("");
});
$(document).off('submit',"#post_form-<%= post.id %>"); // without this the form submitted multiple times in production
$(document).on('submit', '#post_form-<%= post.id %>', function(e) { // don't use $('#post_form-<%= post.id %>').submit(function() { so it doesn't bind to the #post_form (so it still works after ajax loading)
e.preventDefault(); // prevent normal form submission
if ( validatePostForm('<%= post.id %>') ) {
$.ajax({
type: 'POST',
url: $(this).attr('action'),
data: $(this).serialize(),
dataType: 'script'
});
$('#postCancel-<%= post.id %>').trigger("click");
}
});
function validatePostForm(postid) {
if ( jQuery.isBlank($('#postfield-' + postid).val()) && jQuery.isBlank($('#postFile-' + postid).val()) ) {
alert("Write something fascinating or add a picture.");
return false;
} else {
return true;
}
}
Javascript in application.js
function prepareUpload(feckid) {
$(feckid).find("input:file").each(function(i, elem) {
var fileInput = $(elem);
var progressBar = $("<div class='progBar'></div>");
var barContainer = $("<div class='progress'></div>").append(progressBar);
fileInput.after(barContainer);
var maxFS = 10 * 1024 * 1024;
var info = $(".info");
var urlnumbnuts = info.attr("data-url");
var formdatanumbnuts = jQuery.parseJSON(info.attr("data-formdata"));
var hostnumbnuts = info.attr("data-host");
var form = $(fileInput.parents('form:first'));
fileInput.fileupload({
fileInput: fileInput,
maxFileSize: maxFS,
url: urlnumbnuts,
type: 'POST',
autoUpload: true,
formData: formdatanumbnuts,
paramName: 'file',
dataType: 'XML',
replaceFileInput: false,
add: function (e, data) {
$.each(data.files, function (index, file) {
if (file.size > maxFS) {
alert('Alas, the file exceeds the maximum file size of 10MB.');
form[0].reset();
return false;
} else {
data.submit();
return true;
}
});
},
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
progressBar.css('width', progress + '%')
},
start: function (e) {
progressBar.
css('background', 'orange').
css('display', 'block').
css('width', '0%').
text("Preparing...");
},
done: function(e, data) {
var key = $(data.jqXHR.responseXML).find("Key").text();
var url = '//' + hostnumbnuts + '/' + key;
var input = $('<input />', { type:'hidden', class:'appendedInput',
name: fileInput.attr('name'), value: url });
form.append(input);
progressBar.
css('background', 'green').
text("Ready");
},
fail: function(e, data) {
progressBar.
css("background", "red").
css("color", "black").
text("Failed");
}
});
});
} // function prepareUpload()
create.js.erb
$(".info").attr("data-formdata", '<%=raw #s3_direct_post.fields.to_json %>'); // don't use .data() to set attributes
$(".info").attr("data-url", "<%= #s3_direct_post.url %>");
$(".info").attr("data-host", "<%= URI.parse(#s3_direct_post.url).host %>");
$('.post_form')[0].reset();
$('.postText').val('');
application.js
//= require jquery-fileupload/basic
config/initializers/aws.rb
Aws.config.update({
region: 'us-east-1',
credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})
S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])
Notes:
This solution is designed for multiple post forms on the index.html.erb page. This is why the #s3_direct_post information is placed inside a div of class info inside index.html.erb, rather than in each post form. This means there is only one #s3_direct_post presented on the page at any one time, irrespective of the number of forms on the page. The data inside the #s3_direct_post is only grabbed (with a call to prepareUpload()) upon clicking the file upload button. Upon submission a fresh #s3_direct_post is generated in the posts controller, and the information inside .info is updated by create.js.erb. Storing the #s3_direct_post data inside the form means many different instances of #s3_direct_post can exist at once, leading to errors with the file name generation.
You need to :set_s3_direct_post in both the posts controller index action (ready for the first upload) and the create action (ready for the second and subsequent uploads).
Normal form submission is prevented by e.preventDefault(); so it can be done 'manually' with $.ajax({. Why not just use remote: true in the form? Because in Rails, file upload is done with an HTML request and page refresh even when you try to do it remotely.
Use info.attr() rather than info.data() to set and retrieve the #s3_direct_post attributes because info.data doesn't get updated
(for example see this question). This means you also have to manually parse the attribute into an object using jQuery.parseJSON() (which .data() actually does automatically).
Don't use //= require jquery-fileupload in application.js. This bug was a real ballache to identify (see here). The original Heroku solution didn't work until I changed this.
You can use Paperclip to upload to S3 (see documentation) and to create thumbnails, although it uploads to temporary folder first, after that image processing can be applied before uploading file to S3.
As for the examples of such configuration, there are plenty of them throughout the blogosphere and on StackOverflow, e.g. this.

Resources