I'm writing some image upload code for Ruby on Rails with Paperclip, and I've got a working solution but it's very hacky so I'd really appreciate advice on how to better implement it. I have an 'Asset' class containing information about the uploaded images including the Paperclip attachment, and a 'Generator' class that encapsulates sizing information. Each 'Project' has multiple assets and generators; all Assets should be resized according to the sizes specified by each generator; each Project therefore has a certain set of sizes that all of its assets should have.
Generator model:
class Generator < ActiveRecord::Base
attr_accessible :height, :width
belongs_to :project
def sym
"#{self.width}x#{self.height}".to_sym
end
end
Asset model:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
# this is utterly horrendous
def styles
s = {}
if @generators == nil
@generators = self.project.generators
end
@generators.each do |g|
s[g.sym] = "#{g.width}x#{g.height}"
end
s
end
end
Asset controller create method:
def create
@project = Project.find(params[:project_id])
@asset = Asset.new
@asset.generators = @project.generators
@asset.update_attributes(params[:asset])
@asset.project = @project
@asset.uploaded_by = current_user
respond_to do |format|
if @asset.save_(current_user)
@project.last_asset = @asset
@project.save
format.html { redirect_to project_asset_url(@asset.project, @asset), notice: 'Asset was successfully created.' }
format.json { render json: @asset, status: :created, location: @asset }
else
format.html { render action: "new" }
format.json { render json: @asset.errors, status: :unprocessable_entity }
end
end
end
The problem I am having is a chicken-egg issue: the newly created Asset doesn't know which generators (size specifications) to use until after it's been instantiated properly. I tried using @project.assets.build, but then the Paperclip code is still executed before the Asset gets its project association set and nils out on me.
The 'if @generators == nil' hack is so the update method will work without further hacking in the controller.
All in all it feels pretty bad. Can anyone suggest how to write this in a more sensible way, or even an approach to take for this kind of thing?
Thanks in advance! :)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With