Ruby on Rails - Paperclip and dynamic parameters

五迷三道 提交于 2019-12-31 15:15:13

问题


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! :)


回答1:


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.




回答2:


While I really like Cade's solution, just a suggestion. It seems like the 'styles' belong to a project...so why aren't you calculating the generators there?

For example:

class Asset < ActiveRecord::Base
  attr_accessible :filename,
  :image # etc.
   attr_accessor :generators

   has_attached_file :image,
     :styles => lambda { |a| a.instance.project.styles }
end


 class Project < ActiveRecord::Base
   ....

   def styles
     @generators ||= self.generators.inject {} do |hash, g|
       hash[g.sym] = "#{g.width}x#{g.height}"
     end
   end
end

EDIT: Try changing your controller to (assuming the project has many assets):

def create
  @project = Project.find(params[:project_id])
  @asset = @project.assets.new
  @asset.generators = @project.generators
  @asset.update_attributes(params[:asset])
  @asset.uploaded_by = current_user
end



回答3:


I've just solved a similar problem that I had. In my "styles" lambda I am returning a different style depending on the value of a "category" attribute. The problem though is that Image.new(attrs), and image.update_attributes(attrs) doesn't set the attributes in a predictable order, and thus I can't be guaranteed that image.category will have a value before my styles lambda is called. My solution was to override attributes=() in my Image model as follows:

class Image
  ...
  has_attached_file :image, :styles => my_lambda, ...
  ...
  def attributes=(new_attributes, guard_protected_attributes = true)
    return unless new_attributes.is_a?(Hash)
    if new_attributes.key?("image")
      only_attached_file    = {
        "image" => new_attributes["image"]
      }
      without_attached_file = new_attributes
      without_attached_file.delete("image") 
      # set the non-paperclip attributes first
      super(without_attached_file, guard_protected_attributes)
      # set the paperclip attribute(s) after
      super(only_attached_file, guard_protected_attributes)
    else
      super(new_attributes, guard_protected_attributes)
    end
  end
  ...
end

This ensures that the paperclip attribute is set after the other attributes and can thus use them in a :style lambda.

It clearly won't help in situations where the paperclip attribute is "manually" set. However in those circumstances you can help yourself by specifying a sensible order. In my case I could write:

image = Image.new
image.category = "some category"
image.image = File.open("/somefile") # styles lambda can use the "category" attribute
image.save!

(Paperclip 2.7.4, rails 3, ruby 1.8.7)



来源:https://stackoverflow.com/questions/14305018/ruby-on-rails-paperclip-and-dynamic-parameters

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!