Ruby on Rails - Paperclip and dynamic parameters
I ran into the same Paperclip chicken/egg issue on a project trying to use dynamic styles based on the associated model with a polymorphic relationship. I've adapted my solution to your existing code. An explanation follows:
class Asset < ActiveRecord::Base
attr_accessible :image, :deferred_image
attr_writer :deferred_image
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
after_save :assign_deferred_image
def styles
project.generators.each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
private
def assign_deferred_image
if @deferred_image
self.image = @deferred_image
@deferred_image = nil
save!
end
end
end
Basically, to get around the issue of Paperclip trying to retrieve the dynamic styles before the project relation information has been propagated, you can assign all of the image
attributes to a non-Paperclip attribute (in this instance, I have name it deferred_image
). The after_save
hook assigns the value of @deferred_image
to self.image
, which kicks off all the Paperclip jazz.
Your controller becomes:
# AssetsController
def create
@project = Project.find(params[:project_id])
@asset = @project.assets.build(params[:asset])
@asset.uploaded_by = current_user
respond_to do |format|
# all this is unrelated and can stay the same
end
end
And the view:
<%= form_for @asset do |f| %>
<%# other asset attributes %>
<%= f.label :deferred_upload %>
<%= f.file_field :deferred_upload %>
<%= f.submit %>
<% end %>
This solution also allows using accepts_nested_attributes
for the assets
relation in the Project
model (which is currently how I'm using it - to upload assets as part of creating/editing a Project).
There are some downsides to this approach (ex. validating the Paperclip image
in relation to the validity of the Asset
instance gets tricky), but it's the best I could come up with short of monkey patching Paperclip to somehow defer execution of the style
method until after the association information had been populated.
I'll be keeping an eye on this question to see if anyone has a better solution to this problem!
At the very least, if you choose to keep using your same solution, you can make the following stylistic improvement to your Asset#styles
method:
def styles
(@generators || project.generators).each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
Does the exact same thing as your existing method, but more succinctly.
Paperclip dynamic url?
Completely scrap the whole idea of dynamic URL parameter given to the Paperclip attachment. It breaks S3 image uploading cause Paperclip can't figure out which URL to use.
The solution is to introduce a new column in your schema called image_url
.
The column will be updated on initialize/update in the ActiveModel and used in the web pages.
In code
class Product
has_attached_file: :image
after_create :update_image_url
after_update :update_image_url
def update_image_url
new_image_url = # some logic based on image_file_name that would either return the CloudFront URL or save the URL from image.url which is generated by Paperclip
# Paperclip does not update image_file_name_changed? so we can't say
# after_create or after_update if: image_file_name_changed? instead
# we have to manually check that image_url and new_image_url are different
update(image_url: new_image_url) if image_url != new_image_url
end
end
Dynamic use of :default_url in Paperclip
I found a solution, following this gist and this other question in stackoverflow.
My working solution:
Class Service
has_attached_file :logo,
:path => "/:id-:style-:filename",
:url => ":s3_eu_url",
:default_url => :set_default_url_on_category,
:styles => { :large => "600x400>",
:medium => "300x200>",
:small => "100x75>",
:thumb => "60x42>" }
private
def set_default_url_on_category
"/logos/:style/#{category.name}.png"
end
end
And an initializer paperclip_default_url_fix.rb
module Paperclip
module Interpolations
def self.interpolate(pattern, *args)
pattern = args.first.instance.send(pattern) if pattern.kind_of? Symbol
all.reverse.inject(pattern.dup) do |result, tag|
result.gsub(/:#{tag}/) do |match|
send(tag, *args)
end
end
end
end
end
Paperclip dynamic styles based on polymorphic parent class doesn't work (Rails 4.2.5, Paperclip 4.3.1)
The monkey-patch solution explains pretty much about why this is happening. But it's not easy to understand if you don't have comprehensive knowledge about Active Record. Basically, you have to make Rails assign imageable related attributes before assigning the Paperclip ones.
I've found a simpler solution thanks to @Eren CAY here. But I made some modifications for it to work better in my app.
In my Image
model:
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
has_attached_file :file,
:styles => -> (file) {file.instance.get_customized_styles},
:default_url => "/images/:style/missing.png"
def get_customized_styles
raise "Imageable not found." unless imageable_type
if imageable_type == "List"
imageable_type.constantize.image_styles
else
raise "Please define styles for #{imageable_type}."
end
end
end
And in my lists_controller.rb
:
class ListsController < ApplicationController
def list_params
params.require(:list).permit(:title, :description, :image_attributes)
end
def new
@list = List.new
@list.build_image
end
def create
cleaned_list_params = list_params.dup
cleaned_list_params.delete(:image_attributes)
@list = List.new(cleaned_list_params)
image_params = {imageable_type: "List", file: params[:list][:image_attributes][:file]}
@list.build_image(image_params)
if @list.save
redirect_to :action => "index"
else
render :action => "new"
end
end
end
I think the essential is the specified parameter passed to tell Image
its imageable type (no matter the parameter is a string or object or sth else). Normally, it's only after the file attribute is assigned that other imageable related attributes are assigned.
The main differences from the original solution is that:
- Pass only a string rather than a object to image. Then constantize the string if it's needed.
- Store image_styles in imageable models. I prefer this way to maintain the styles rather than put them all in
Image
model. - Pass strong parameters to
List.new
as it has its own attributes. (The 'clean' process is optional, I just don't want the whole bunch of image_attributes to be passed and trigger Paperclip)
paperclip - dynamic use of :path
We can use Paperclip.interpolates for this work.
class Attachment < ActiveRecord::Base
self.table_name = 'attachments'
self.primary_key = 'srl'
validates :document_srl,
:presence => true,
:numericality => { only_integer: true },
allow_nil: false
has_attached_file :attached,
:path => ":attachment/:document_srl/:id/:style/:filename"
validates_attachment_content_type :attached, :content_type => /\Aimage\/.*\Z/
Paperclip.interpolates :document_srl do |attachment, style|
attachment.instance.document_srl
end
end
Dynamic Attachment Size for Paperclip (Rails)
Paperclip doesn't allow to pass function as size limit parameter. So you probably need to write custom validation:
validate :validate_image_size
def validate_image_size
if document.file? && document.size > get_current_file_size_limit
errors.add_to_base(" ... Your error message")
end
end
Paperclip - dropbox. How can I setup a conditional has_attached_file?
I'm going to answer my own question because the other answers were too vague to accept - although they were on the right path. I think the community would prefer an answer with more code to back it up.
So here goes. To change the has_attached_file
on a dynamic basis, you have to have a user_id
column in the attachment model so that you're not calling current_user
(which isn't possible without ugly hacks). Then you need a belongs_to
as well to complete the user association. Let's assume I'm attaching an audio file to a Song
model for this example.
The key to getting the dynamically set variables is to initialize the attachment with the after_initialize
callback.
Song.rb
belongs_to :user
has_attached_file :audio
after_initialize :init_attachment
def init_attachment
self.class.has_attached_file :audio,
:storage => :dropbox,
:dropbox_credentials => {app_key: DROPBOX_KEY,
app_secret: DROPBOX_SECRET,
access_token: self.user.token,
access_token_secret: self.user.secret,
user_id: self.user.id
access_type: "app_folder"},
:dropbox_options => {}
end
You are, of course, free to setup your association differently, but this is a working code example for the question posed.
Paperclip :style depending on model (has_many polymorphic images)
I am really late to the party here, but I wanted to clarify something about accessing model data for anyone else that happens on to this thread. I just faced this issue while using Paperclip to apply watermarks based on data from the model, and got it working after a lot of investigation.
You said:
After reading a lot on the Paperclip forum I don't believe it's possible to access the instance before it has been saved. You can only see the Paperclip stuff and that's it.
In fact, you can see the model data if it's been set in the object before your attachment is assigned!
Your paperclip processors and whatnot are invoked when the attachment in your model is assigned. If you rely on mass assignment (or not, actually), as soon as the attachment is assigned a value, paperclip does its thing.
Here's how I solved the problem:
In my model with the attachment (Photo), I made all the attributes EXCEPT the attachment attr_accessible
, thereby keeping the attachment from being assigned during mass assignment.
class Photo < ActiveRecord::Base
attr_accessible :attribution, :latitude, :longitude, :activity_id, :seq_no, :approved, :caption
has_attached_file :picture, ...
...
end
In my controller's create method (for example) I pulled out the picture
from the params
, and then created the object. (It's probably not necessary to remove the picture from params
, since the attr_accessible
statement should prevent picture
from being assgined, but it doesn't hurt). Then I assign the picture
attribute, after all the other attributes of the photo object have been setup.
def create
picture = params[:photo].delete(:picture)
@photo = Photo.new(params[:photo])
@photo.picture = picture
@photo.save
...
end
In my case, one of the styles for picture calls for applying a watermark, which is the text string held in the attribution
attribute. Before I made these code changes, the attribution string was never applied, and in the watermark code, attachment.instance.attribution
was always nil
. The changes summarized here made the entire model available inside the paperclip processors. The key is to assign your attachment attribute last.
Hope this helps someone.
Related Topics
Does Rails Come with a "Not Authorized" Exception
How to Enable Compression in Ruby on Rails
Rails Way to Detect Mobile Device
Ruby Array to Hash: Each Element the Key and Derive Value from It
Set Tag Attribute and Add Plain Text Content to the Tag Using Nokogiri Builder (Ruby)
Elasticsearch Terms Aggregation by Entire Field
Rbenv: Ruby: Command Not Found
Ruby - Remove Pattern from String
Sunspot_Rails Gem - " Errno:: Econnrefused (Connection Refused - Connect (2)) "
How to See the Ruby Code in a Proc
How to Sort a Ruby Hash Alphabetically by Keys
Memory Size of a Hash or Other Object
Are There Ruby Equivalents to Car, Cdr, and Cons
How to Run and Debug Ruby on Rails from Visual Studio Code
Ruby, Tor and Net::Http::Proxy