Ruby on Rails - Paperclip和动态参数

8
我正在使用Paperclip为Ruby on Rails编写一些图片上传代码,我已经有一个工作解决方案,但它非常繁琐,所以我真的很想得到如何更好地实现它的建议。我有一个“Asset”类,包含有关上传图像的信息,包括Paperclip附件,还有一个“Generator”类,封装了大小信息。每个“Project”都有多个资产和生成器;所有“Asset”应根据每个生成器指定的大小进行调整大小;因此,每个项目都有一定的大小集,所有其资产应该具有这些大小。
生成器模型:
class Generator < ActiveRecord::Base
  attr_accessible :height, :width

  belongs_to :project

  def sym
    "#{self.width}x#{self.height}".to_sym
  end
end

资产模型:

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

资产控制器创建方法:

  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

我遇到的问题是一个鸡生蛋蛋生鸡的问题:新创建的“资产”在正确实例化之前不知道要使用哪些生成器(大小规范)。我尝试使用@project.assets.build,但是在Asset获取其项目关联设置和nil值之前,仍会执行Paperclip代码,导致我的程序崩溃。
“如果@generators == nil”的hack是为了使更新方法在不进一步修改控制器的情况下正常工作。
总而言之,这感觉很糟糕。有人可以建议如何更明智地编写此代码,甚至提出这种情况的解决方案吗?
提前谢谢! :)

这个问题及其答案对我在应用程序中构建相同功能有很大帮助。感谢大家! :) - Gediminas Šukys
3个回答

15

我在一个使用多态关系基于相关模型的动态样式尝试的项目中遇到了与你相同的Paperclip鸡蛋问题。我已经将我的解决方案适应你现有的代码。下面是一个解释:

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

基本上,为了解决Paperclip尝试在项目关系信息传播之前检索动态样式的问题,你可以将所有image属性分配给一个非Paperclip属性(在这种情况下,我把它命名为deferred_image)。after_save钩子将@deferred_image的值分配给self.image,从而启动所有的Paperclip操作。

你的控制器变成了:

# 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

并且视图:

<%= form_for @asset do |f| %>
  <%# other asset attributes %>
  <%= f.label :deferred_upload %>
  <%= f.file_field :deferred_upload %>
  <%= f.submit %>
<% end %>

这个解决方案还允许在Project模型的assets关系中使用accepts_nested_attributes(这也是我目前正在使用的方式——将资产上传为创建/编辑项目的一部分)。

这种方法存在一些缺点(例如,在与Asset实例的有效性相关联时验证Paperclip的image变得棘手),但除了猴子补丁Paperclip以某种方式推迟执行style方法直到关联信息被填充之后,这是我能想到的最好的办法。

我会继续关注这个问题,看看是否有更好的解决方案!


至少,如果你选择继续使用你的解决方案,可以对你的Asset#styles方法进行以下样式上的改进:

def styles
  (@generators || project.generators).each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end

与您现有的方法完全相同,但更加简洁。


5

虽然我非常喜欢Cade的解决方案,但我有一个建议。看起来“styles”属于项目......所以为什么不在那里计算生成器呢?

例如:

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

编辑:尝试将控制器更改为以下内容(假设项目有许多资产):

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

虽然确定实际样式方法的位置是一个有趣的问题,但这对于新记录不起作用。在样式lambda中,直到记录被保存之前,a.instance.project将为nil,因此您将在nil上获得NoMethodError - Cade
我编辑了我的解决方案...我确定实例必须指向@asset变量?如果您像我一样更改控制器,那么它应该已经将项目放在一个隐藏的实例变量中供paperclip使用... - Joe Pym
Rails在Project模型的styles动作上抛出了一个错误,提示:语法错误,意外的关键字_do_block,期望关键字end - Gediminas Šukys

3

我刚刚解决了一个类似的问题。在我的“styles”lambda中,我返回不同的样式,具体取决于“category”属性的值。但问题是,Image.new(attrs)和image.update_attributes(attrs)不能以可预测的顺序设置属性,因此我无法保证在调用我的styles lambda之前,image.category将有一个值。我的解决方案是在我的Image模型中覆盖attributes=()方法,如下所示:

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

这可以确保paperclip属性在其他属性之后设置,因此可以在:style lambda中使用它们。
当paperclip属性是“手动”设置时,这显然没有帮助。但是,在这种情况下,您可以通过指定明智的顺序来帮助自己。在我的情况下,我可以写:
image = Image.new
image.category = "some category"
image.image = File.open("/somefile") # styles lambda can use the "category" attribute
image.save!

(纸夹2.7.4,rails 3,ruby 1.8.7)

(注:无需翻译)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接