Rails 4 paperclip Background job - ruby-on-rails-4

CURRENT LOGIC
First, I think I have read all the articles on paperclip and I'm still stucked despite all the infos learned... So I can say taht your help is really really precious.
Secondly, I do not use delayed_paperclip, nor s3_direct_upload (but jquery directUpload).
I have users profile with 4 differents pictures : logo, avatar, worka, workb
Each format (logo, avatar, worka, workb) has 3 styles (:small, :thumb, :medium)
User can update its pictures => so update action is concerned
The 4 files fields are in a form with other classic fields (name, email, ...)
Upload is managed by jQuery DirectUpload + Paperclip
When user click the file field to add an image:
jQuery DirectUpload uploads the file into a temp directory on s3
jQuery callbacks with the url(key)
The url is assigned as :temp to an hidden field generated in javascript
When form submit button is pressed:
I assign to paperclip the url of the file uploaded with direct upload, with the help of #user.logo.temp which contains the url(key)
Paperclip generates all styles
<input type="hidden" name="user[logo_attributes][temp]" value="https://bucketname.s3.amazonaws.com/temp/e4b46d01-5d69-483b.jpg">
MY PROBLEM: STYLES GENERATION BRINGS ME TO HEROKU IDLE and TIMEOUT #15 #12
.
ATTEMPTS: I tried to isolate the upload process to put it in a background job
.
I can't figure out how to block paperclip styles generation at the first upload and generate them after in a background job
before_post_process block all post process, even in background job
I didn't use .reprocess! Since paperclip 4, update is triggered and... infinite loop..., so I use .assign and .save instead
The file is correctly assigned from S3 hosted file, then processed by paperclip
I'm not sure about the file from file field, if it is uploaded or not (no trace of that in the console, but since the form is submited, the file too, even if unused.
.
NEED: STYLES BLOCKED THEN PROCESSING IN A BACKGROUND JOB
My Logo Model
class Logo < Document
S3_TEMP_URL_FORMAT = %r{\/\/bucketname\.s3\.amazonaws\.com\/(?<path>temp\/.+\/(?<filename>.+))\z}.freeze
has_attached_file :attachment,
styles: { medium: "300x300#" },
convert_options: { medium: "-quality 75 -strip" },
default_url: ":parent_type/:class/:style/missing.png",
path: "/documents/:parent_type/:id_partition/:class/:style/:basename.:extension"
validates_attachment :attachment,
content_type: { content_type: ["image/gif", "image/png", "image/jpg", "image/jpeg"] },
size: { less_than: 1.megabyte }
validates :temp,
# presence: true,
format: { with: S3_TEMP_URL_FORMAT }
before_save :set_attachment
after_save :set_remote_url
before_post_process :stop_process
def stop_process
false
end
def styles_process
self.attachment.assign(attachment)
self.save
end
def set_attachment
# puts "BEGIN -- SET ATTACHMENT"
tries ||= 5
s3_temp_url_data = S3_TEMP_URL_FORMAT.match(self.temp)
s3 = AWS::S3.new
s3_temp_head = s3.buckets[ENV['S3_BUCKET']].objects[s3_temp_url_data[:path]].head
self.attachment_file_name = s3_temp_url_data[:filename]
self.attachment_file_size = s3_temp_head.content_length
self.attachment_content_type = s3_temp_head.content_type
self.attachment_updated_at = s3_temp_head.last_modified
rescue AWS::S3::Errors::NoSuchKey => e
tries -= 1
if tries > 0
sleep(3)
retry
else
false
end
end
def set_remote_url
s3_temp_url_data = S3_TEMP_URL_FORMAT.match(self.temp)
s3 = AWS::S3.new
self.attachment = URI.parse(self.temp)
self.save
s3.buckets[ENV['S3_BUCKET']].objects.with_prefix(s3_temp_url_data[:path]).delete_all
end
end
My Controller
def update
account_update_params = devise_parameter_sanitizer.sanitize(:account_update)
#user = User.find(current_user.id)
if #user.update_attributes(account_update_params)
# Here is the styles processing
# This is where the Resque background job would go
#user.logo.styles_process
set_flash_message :notice, :updated
redirect_to after_update_path_for(#user)
else
render :edit
end
end
My Form
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), method: :put, html: { class: "form-horizontal directUpload", role: "form" }) do |f| %>
<%= f.fields_for :logo do |l| %>
<%= l.file_field(:attachment, accept: 'image/gif,image/png,image/jpg,image/jpeg') %>
<% end %>
<input type="hidden" name="user[logo_attributes][temp]" value="https://bucketname.s3.amazonaws.com/temp/e4b46d01-5d69-483b.jpg">
<% end %>

Related

Inject data into Sass variables in Ember application

I'm working on an application that renders many, separate "sites" as subdirectories- e.g. /client/1, /client/2, etc. For each of these, two color values can be specified in the admin portion of the application.
I'd like to know if there's a method to inject the values, which were initially posted to and then retrieved from the back-end API by Ember, into a SCSS file for preprocessing?
I've found no solution thus far.
In our Ember/Rails application, we are generating CSS files for each client based on some settings in the database. For example, our Tenant model has two fields:
{
primary_color: 'ff3300',
secondary_color: '00ff00'
}
We expose routes
scope '/stylesheets', format: 'css' do
get 'tenant/:tenant_id', to: 'stylesheets#tenant_css'
end
And our controller looks something like this:
class StylesheetsController < ApplicationController
layout nil
rescue_from 'ActiveRecord::RecordNotFound' do
render nothing: true, status: 404
end
def tenant_css
# fetch model
tenant = Tenant.find(params[:tenant_id])
# cache the css under a unique key for tenant
cache_key = "tenant_css_#{tenant.id}"
# fetch the cache
css = Rails.cache.fetch(cache_key) do
# pass sass "params"
render_css_for 'tenant', {
primary_color: tenant.primary_color,
secondary_color: tenant.secondary_color
}
end
render_as_css css
end
protected
# our renderer, could also add a custom one, but simple enough here
def render_as_css(css)
render text: css, content_type: 'text/css'
end
# looks for a template in views/stylesheets/_#{template}.css.erb
def render_css_for(template, params = {})
# load the template, parse ERB w params
scss = render_to_string partial: template, locals: { params: params }
load_paths = [Rails.root.join('app/assets/stylesheets')]
# parse the rendered template via Saas
Sass::Engine.new(scss, syntax: :scss, load_paths: load_paths).render
end
end
This way, you can link to /stylesheets/tenant/1.css which will render the CSS for the tenant using the Sass engine.
In this case, in views/stylesheets/_tenant.css.erb, you'd have something like this (it's an ERB file but you can use Sass in there now):
#import "bootstrap-buttons";
<% if params[:primary_color].present? %>
$primary-color: <%= params[:primary_color] %>;
h1, h2, h3, h4, h5, h6 {
color: $primary-color;
}
<% end %>
<% if params[:secondary_color].present? %>
$secondary-color: <%= params[:secondary_color] %>;
a {
color: $secondary-color;
&:hover {
color: darken($secondary-color, 10%);
}
}
<% end %>
You'll note that I can now use #import to import anything in your stylesheet path for the Sass engine (in this case, I can utilize some helpers from Bootstrap Sass lib).
You'll want to have some sort of cache cleaner to wipe the cache when your model backing the CSS is updated:
class Tenant < ActiveRecord::Base
after_update do
Rails.cache.delete("tenant_css_#{id}")
end
end
So that's the Rails side in a nutshell.
In Ember, my guess is you'll want to load the stylesheet based on an ID, so that stylesheet cannot be hard-coded into "index.html". Ember CSS Routes addon might serve you well, but I found that it just appends <link> to the header, so if you need to swap CSS stylesheets at any time, this won't work. I got around this in a route like so:
afterModel(model, transition) {
// dynamically form the URL here
const url = "/stylesheets/tenant/1";
// build link object
const $link = $('<link>', { rel: 'stylesheet', href: url, id: 'tenant-styles' });
// resolve the promise once the stylesheet loads
const promise = new RSVP.Promise((resolve, reject) => {
$link.on('load', () => {
$link.appendTo('head');
resolve();
}).on('error', () => {
// resolve anyway, no stylesheet in this case
resolve();
});
});
return promise;
},
// remove the link when exiting
resetController(controller, isExiting, transition) {
this._super(...arguments);
if (isExiting) {
$('#tenant-styles').remove();
}
}
You could also add a blank element in the <head> and then use Ember Wormhole to format a <link> tag and render into the "wormhole".
Edit
You could also look into rendering Sass directly in the client application. For something as simple as two colors, this wouldn't have much of performance impact, especially if you used a service worker or similar to cache the results.

passing an id in Rails 4

If i start this question by showing you my routes: -
c:\Sites\work\easygifts>rake routes
Prefix Verb URI Pattern Controller#Action
writing_stores GET /stores/writing(.:format) stores#writing
office_stores GET /stores/office(.:format) stores#office
time_stores GET /stores/time(.:format) stores#time
home_stores GET /stores/home(.:format) stores#home
wellness_stores GET /stores/wellness(.:format) stores#wellness
travel_stores GET /stores/travel(.:format) stores#travel
bags_stores GET /stores/bags(.:format) stores#bags
leisure_stores GET /stores/leisure(.:format) stores#leisure
quote_stores GET /stores/quote(.:format) stores#quote
stores GET /stores(.:format) stores#index
POST /stores(.:format) stores#create
new_store GET /stores/new(.:format) stores#new
edit_store GET /stores/:id/edit(.:format) stores#edit
store GET /stores/:id(.:format) stores#show
PATCH /stores/:id(.:format) stores#update
PUT /stores/:id(.:format) stores#update
DELETE /stores/:id(.:format) stores#destroy
products GET /products(.:format) products#index
POST /products(.:format) products#create
new_product GET /products/new(.:format) products#new
edit_product GET /products/:id/edit(.:format) products#edit
product GET /products/:id(.:format) products#show
PATCH /products/:id(.:format) products#update
PUT /products/:id(.:format) products#update
DELETE /products/:id(.:format) products#destroy
root GET / stores#index
The issue i am having is getting the :id into the 'quote' view.
I am wanting to see in my routes; quote_stores GET /stores/quote/:id(.:format) stores#quote Or something like it.
Can :id only be passed through CRUD?? I thought i could pass instance variables through pretty much anywhere so I wrote this in my view as the link to the view with the :id info passed into it.
<% #products.each do |office| %>
<div class="item">
<%= link_to image_tag(office.image_url), image_path(office.image_url), class: 'fancybox' %>
<p><strong><%= office.item_code%></strong>
</br><em><%= truncate(office.title, length: 18) %></em></p>
<p class="showArticle"><%= link_to 'Show Article', store_path(office) %></p>
<p class="addTo"><%= link_to 'Price Info', quote_stores_path(office) %></p>
</div>
<% end %>
I am referring to the <%= link_to 'Price Info', quote_stores_path(office) %> which upon click takes you to the correct view and in the URI path it even lists the correct :id however it does not pass into the view that :id's information.
My controller code is as follows: -
class StoresController < ApplicationController
add_breadcrumb 'home', :stores_path
def index
#products = Product.all
end
def show
#products = Product.find(params[:id])
if #products.nil?
redirect_to action: :index
end
add_breadcrumb 'Back', #products.section
end
def writing
#products = Product.where(:section => 'writing').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'writing', writing_stores_path
end
def office
#products = Product.where(:section => 'office').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'office', office_stores_path
end
def time
#products = Product.where(:section => 'time').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'time', time_stores_path
end
def home
#products = Product.where(:section => 'home').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'home', home_stores_path
end
def wellness
#products = Product.where(:section => 'wellness').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'wellness', wellness_stores_path
end
def travel
#products = Product.where(:section => 'travel').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'travel', travel_stores_path
end
def bags
#products = Product.where(:section => 'bags').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'bags', bags_stores_path
end
def leisure
#products = Product.where(:section => 'leisure').paginate(:per_page => 5, :page => params[:page])
add_breadcrumb 'leisure', leisure_stores_path
end
def quote
#products = Product.find_by(params[:id])
if #products.nil?
redirect_to action: :index
end
end
end
So apart from my code not being DRY, what am i missing here please? What am i not understanding about :id's?
In your config/routes.rb, you probably have this line:
resources :stores
Which creates routes for the standard CRUD actions of the stores resource. You can define additional actions for this resource which either apply to the collection (multiple products) or the members individually (a single product).
Please note that it would probably more logical to name the resource products rather than stores, since it seems to be handling Products.
In your case, you'd want to define an additional member action. Since it applies to a single product, Rails will define a route which takes an id parameter:
resources :stores do
member do
get 'quote'
end
end
This will generate the following route (rake routes):
quote_store GET /stores/:id/quote(.:format) stores#quote
Note that the route is called quote_store, the singular form rather than the plural.
Also see the Rails guides for more information about collection and member routes.

How to send multiple files using dropzone.js with Rails?

Using Rails 4 Paperclip and SimpleForm.
I'm trying to make a multiple file upload. For this I am using Dropzone.js upon customer request.
It is a form where I have: Name, Address, Phone, Documents, Certificates.
So I created 2 separate dropzones for documents and certificates.
This is my code on the form:
= simple_form_for(#user, html: {multipart: true, autocomplete: 'off' }) do |f|
= f.input :name, label: false # Column name in table User
= f.input :address, label: false # Column address in table User
#attachments-documents.dropzone # Column document in table User
#attachments-certificates.dropzone # Column certificate in table User
:javascript
var attachments_1 = new Dropzone("div#attachments-documents", { url: "#{upload_file_biddings_path}"});
var attachments_2 = new Dropzone("div#attachments-certificates", { url: "#{upload_file_path}"});
Dropzone.options.attachmentsDocuments = {
paramName: 'user[document]',
maxFilesize: 20,
parallelUploads: 3,
addRemoveLinks : true,
autoProcessQueue: false
}
Dropzone.options.attachmentsCertificates = {
paramName: 'user[certificate]',
maxFilesize: 20,
parallelUploads: 3,
addRemoveLinks : true,
autoProcessQueue: false
}
Controller:
Class User < ApplicationController
[...]
def create
work = Work.new(work_params)
if work.save
flash[:notice] = "Se ha creado correctamente la Obra."
redirect_to :action => :index
else
work.errors.messages.each do |attribute, error|
puts error.to_s
puts error
end
flash[:error] = "Ha ocurrido un error en el sistema."
redirect_to :action => :index
end
end
def update
work = Work.find(params[:id])
if work.update_attributes(work_params)
flash[:notice] = "Se ha actualizado correctamente los datos."
redirect_to :action => :index
else
work.errors.messages.each do |attribute, error|
flash[:error] = attribute " " + flash[:error].to_s + error.to_s + " "
end
# Load new()
#work = work
render :edit, layout: false
end
end
def upload_file
puts params
render :json => params
end
private
def work_params
params.require(:user).permit(:name, :address, :document, :certificate)
end
end
But here I have 2 problems.
1) When I put a file into the Dropzone, by default calls the "upload_file" function when the files should be uploaded when you click on my submit button and GO to create function.
2) I did a test upload multiple files with another JS, however only managed to climb the last of them. Ie:
I put in my file_field 3 files: file1, file2, file3. But in DB, only recorded the file3.
Has anyone uploaded multiple files with Paperclip? Using JS Dropzone.js or other successfully?
If so, I'd like to share knowledge.

AWS credentials aren't working with Paperclip image upload in Rails 4 and mongoid

I'm receiving the following error:
The AWS Access Key Id you provided does not exist in our records.
I'm not sure why I would be getting this error because I have the correct access key and secret key shown to me in my aws security credentials page (I have copied and pasted multiple times to refute the possibility of a typo). Why would it be telling me that my access key is not in their records if I am able to log in and visit my STILL empty buckets?
Here is my s3.yml:
GMAIL_USERNAME: <my email>
GMAIL_PASSWORD: <my password>
MONGOHQ_URL: <my mongoid url>
S3_BUCKET_NAME_DEVELOPMENT: <my development bucket>
S3_BUCKET_NAME_PRODUCTION: <my production bucket>
S3_BUCKET_NAME_TEST: <my test bucket>
AWS_ACCESS_KEY_ID: <my access key>
AWS_SECRET_ACCESS_KEY: <my secret key>
Here is my picture.rb:
class Picture
include Mongoid::Document
include Mongoid::Paperclip
embedded_in :datum, inverse_of: :pictures
has_mongoid_attached_file :attachment,
path: ':attachment/:id/:style.:extension',
storage: :s3,
url: ':s3_domain_url',
bucket: Proc.new{ |a| a.instance.s3_bucket },
s3_credentials: File.join(Rails.root, 'config', 's3.yml'),
styles: {
original: ['1920x1680', :jpg],
small: ['100x100', :jpg],
medium: ['250x250', :jpg],
large: ['500x500', :jpg]
},
convert_options: { all: '-background white -flatten +matte' }
def s3_bucket
if Rails.env.development?
ENV['S3_BUCKET_NAME_DEVELOPMENT']
else
ENV['S3_BUCKET_NAME_PRODUCTION']
end
end
end
Here is my datum.rb:
class Datum
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Paperclip
field :title, type: String
field :user_id, type: Integer
validates :title, presence: true
validates :user_id, presence: true
embeds_many :todo_items
embeds_many :pictures, cascade_callbacks: true
accepts_nested_attributes_for :pictures
end
Here is my datum_params method:
# Never trust parameters from the scary internet, only allow the white list through.
def datum_params
params.require(:datum).permit(:title, :user_id, :identifying_image, pictures_attributes: [:picture])
end
Here is my picture_params method:
def picture_params
params.require(:picture).permit(:datum_id, :attachment)
end
Here is the form I use for the upload:
<%= bootstrap_form_for(picture, url: { action: :create }, html: { multipart: true }, remote: true ) do |f| %>
<%= f.file_field :picture, multiple: true, name: 'picture[picture]' %>
<%= f.submit 'Upload' %>
<% end %>
Originally, I was having a strong_parameters issue that rails4guides.com (the user with the answer below) was able to help me overcome. I knew I made progress when I stopped getting an unpermitted parameter error and started getting errors from AWS. Can anyone see any reason why my credentials are not working? Am I supposed to be seeing anything about my access key in the request parameters? Because I don't at this time.
Okay, I will explain further in a separate answer to make further elaboration more clear.
In a typical situation you would have something like this in your form :
<%= f.file_field :picture %>
Picture is the name of the file you want to upload. Now imagine this form will pass on it's results to the ImagesController. In order for the ImagesController to be able to process the picture it should be whitelisted through strong parameters, as in:
def image_params
params.require(:image).require(:picture)
end
An other case would be using nested attributes. If you want to set a picture for an article your form will look something like this:
<% f.fields_for :image do |image_form| %>
<%= image_form.file_field :picture %>
<% end %>
And your article_params will look like this:
def article_params
params.require(:article).permit(:title, :content, images_attributes: [:picture])
end
Small note: It should be images_attributes (both plural) and not image_attributes. The actual nested attributes on the other hand should be singular.
This is pretty much a basic outline of strong parameter usage, I hope it will help you. P.s. There might be some typos, since I'm typing this on my mobile phone.

Rails multiple select form: no implicit conversion of String into Integer

In Rails 4, Why isn't an array of genre_id being saved to the database? I think it's because the ids are being read as a string, when it needs to be saved as an integer, but in the past I didn't have this problem.
I get a no implicit conversion of String into Integer error when trying to POST the following:
<%= form_for #project do |f| %>
<%= select_tag 'project[genres_projects_attributes][genre_id][]',
options_for_select(Genre.order(:id).collect{|g| [g.name, g.id]}),
{ include_hidden: true, multiple: true, class: 'form-control'} %>
<% end %>
In my app,
Project has_many :genres, through: :genres_projects
My parameters look like this:
{"utf8"=>"✓","authenticity_token"=>"[xxx]",
"project"=>{"name"=>"sdfds",
"genres_projects_attributes"=>{"genre_id"=>["2",
"3",
"4"]},
"commit"=>"Go!"}
My project_params:
def project_params
params.require(:project).permit(:id, genres_projects_attributes:
[:id, {genre_id: []}, :project_id])
end
Here's the line in my controller that's throwing the error:
def create
#project = Project.create(project_params)
end
Unfortunately didn't find an answer so did the following workaround:
#genre_ids = project_params[:genres_projects_attributes][:genre_id]
#genre_ids.each do |g|
GenresProject.create!(project_id: #project.id, genre_id: g)
end
UPDATE:
I found out that it was the first blank value that was causing the issue. As of Rails 4.x, you can also add include_hidden: false in your form.
UPDATE AGAIN:
if project_params[:genres_projects_attributes]
#project.genres.delete_all
project_params[:genres_projects_attributes][:genre_id].each do |g|
#project.genres << Genre.find(g)
end
#project.update!(project_params.except :genres_projects_attributes)
else
#project.update!(project_params)