10
votes

I have set up my models to use a polymorphic Image model. This is working fine, however I am wondering if it is possible to change the :styles setting for each model. Found some examples using STI (Model < Image) However this is not an option for me, because I am using a has_many relation.

Art

has_many :images, :as => :imageable

Image

belongs_to :imageable, :polymorphic => true
has_attached_file :file, :styles => { :thumb => "150x150>", :normal => "492x600>"}
                         #Change this setting depending on model

UPDATE

I tried starting up a debugger inside the Proc method. Only fields related to the attached file is populated:

run'irb(Image):006:0> a.instance => #<Image id: nil, created_at: nil, updated_at: nil, imageable_id: nil, imageable_type: nil, file_file_name: "IMG_9834.JPG", file_content_type: "image/jpeg", file_file_size: 151326, file_updated_at: "2010-10-30 08:40:23">

This is the object from ImageController#create

ImageController#create
@image => #<Image id: nil, created_at: nil, updated_at: nil, imageable_id: 83, imageable_type: "Art", file_file_name: "IMG_9834.JPG", file_content_type: "image/jpeg", file_file_size: 151326, file_updated_at: "2010-10-30 08:32:49">

I am using paperclip (2.3.5) and Rails 3.0.1. No matter what I do the a.instance object is the image with only the fields related to the attachment populated. Any ideas?

UPDATE2

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 onlye see the Paperclip stuff and that's it.

I got around this problem by presaving the image from the Image controller with a before filter - without the attachment

  before_filter :presave_image, :only => :create

  ...

  private

  def presave_image
    if @image.id.nil? # Save if new record / Arts controller sets @image
      @image = Image.new(:imageable_type => params[:image][:imageable_type], :imageable_id => params[:image][:imageable_id])
      @image.save(:validate => false)
      @image.file = params[:file] # Set to params[:image][:file] if you edit an image.
    end
  end
10
perhaps it is really due to the polymorphic association - or something similar, at least you have now a solution.lwe

10 Answers

8
votes

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.

6
votes

I found the workaround to pickup styles on create. The key is to implement before_post_process and after_save hooks.

class Image < ActiveRecord::Base

  DEFAULT_STYLES = {
    medium: "300x300>", thumb: "100x100>"
  }

  has_attached_file :file, styles: ->(file){ file.instance.styles }


  validates_attachment_content_type :file, :content_type => /\Aimage\/.*\Z/

  # Workaround to pickup styles from imageable model
  # paperclip starts processing before all attributes are in the model
  # so we start processing after saving

  before_post_process ->{
    !@file_reprocessed.nil?
  }

  after_save ->{
    if !@file_reprocessed && (file_updated_at_changed? || imageable_type_changed?)
      @file_reprocessed = true
      file.reprocess!
    end
  }


  belongs_to :imageable, polymorphic: true

  def styles
    if imageable_class.respond_to?(:image_styles)
      imageable_class.image_styles
    end || DEFAULT_STYLES
  end

  def imageable_class
    imageable_type.constantize if imageable_type.present?
  end


end

So you have to define image_styles class method in imageable_class In my case it was

class Property < ActiveRecord::Base

  def self.image_styles
    {
      large: "570x380#",
      thumb: "50x70#",
      medium: "300x200#"
    }
  end

end
3
votes

the :styles property takes a Proc as argument, so you can do all kinds of fancy stuff :)

class Image < AR::Base
  has_attached_file :file, :styles => Proc.new { |a| a.instance.file_styles }

  def file_styles; { :thumb => "150x150>", :normal => "492x600>" } end
end

class Didum < Image
  def file_styles; { :thumb => "50x50>", :normal => "492x600>" } end
end

Note - the above code should work, but honestly I have no setup to verify it, but looks like the Paperclip::Attachment#styles does call if it responds to it, see http://rdoc.info/github/thoughtbot/paperclip/master/Paperclip/Attachment:styles

UPDATE the object passed into the Proc is not the instance, but the Paperclip::Attachment, but the instance is accessible through instance on the attachment

PS: And I've seen this in some other places, but can't remember where...

1
votes
class Banner < ActiveRecord::Base  

  belongs_to :banner_categoria
  validates :banner_categoria_id ,{:presence =>{:message => "não informada"}} 

  has_attached_file :arquivo
  after_initialize :init_attachment




  def init_attachment
    self.class.has_attached_file :arquivo,
      :url => "/system/:class/:attachment/:id/:style/:basename.:extension",
      :path => ":rails_root/public/system/:class/:attachment/:id/:style/:basename.:extension",
      :styles => hash = {
      :banner => {
        :geometry => "#{self.banner_categoria.largura}x#{self.banner_categoria.altura}>",
        :quality => 80
      },
      :thumb => "100x100#"
    }
  end

end

1
votes

I liked MarkGranoff's answer but I came up with a much simpler implementation that does the trick for me and seems more maintainable.

#create new and instantiate the fields you need ( or all except the attachment )
@photo = Photo.new(:attribution => params[:photo][:attribution],
                   :latitude => params[:photo][:latitude ])

#then just assign all params as normal
@photo = Photo.assign_attributes(params[:node])

The second statement assigns the attachment so the processor is fired, and since you've already assigned :attribution and :latitude, their values will be available in the processor via the attachment.instance method.

Thanks everyone here for the insight!

1
votes

I've also met with the same problem and after searching for an elegant solution I didn't found anything "really" useful. So, here's my simple solution which seems alright for now; (tho I'm not sure if it has any downsides for the moment)

In model;

class Asset < ActiveRecord::Base
  belongs_to :assetable, polymorphic: true

  has_attached_file :attachment,  path: ":rails_root/#{path}/assets/images/:style/:filename",
                                  url: '/assets/images/:style/:filename',
                                  styles: -> (a) { a.instance.send(:styles) }
private
  def styles
    raise 'Undefined assetable.' unless assetable

    if assetable.class == Photo
      { small: 'x40', medium: '120x', large: '300x' }
    elsif assetable.class == Avatar
      { small: 'x40', medium: '120x', large: '300x' }
    else
      raise "Styles for #{assetable.class} is not defined."
    end
  end
end

In controller;

@photo = Photo.new
@photo.build_image(assetable: @photo, attachment: params[:photo][:image_attributes][:attachment])
@photo.save
1
votes

After couple of hours of digging into source code of both, ActiveRecord and Paperclip, I think there's a solution involving a bit of monkey-patching hackery.

I haven't thoroughly tested it but seems to work for my humble needs.

Below is my config/initializers/activerecord_associations_patch.rb:

module ActiveRecord
  module Associations
    class HasManyAssociation

      def build_record(attributes)
        if options[:as] && owner
          # Unicorns
          reflection.build_association({}) do |record|
            set_owner_attributes(record)
            unless foreign_key_for?(record)
              record.public_send "#{options[:as]}=", owner
            end
            initialize_attributes(record)
            record.assign_attributes(attributes)
          end
        else
          # Classic Rails way
          reflection.build_association(attributes) do |record|
            initialize_attributes(record)
          end
        end
      end

    end
  end
end

Since reflection doesn't know our @owner, it first assigns the attributes which triggers call to record.#{instance}= (i.e. Image#file=), which in turn forwards it to #assign and performs post_processing hook, which in the end leads to calling styles Proc provided in has_attached_file without polymorphic imageable being set. huh…

I rewrote the method to build the record first and assign provided attributes later. Also, it accounts for record#new_record? in foreign_key_for? check.

In this way attributes are assigned once the record has been properly setup. At least i think so ;)

Cleaner solutions and constructive comments are most welcome :)

0
votes

I've had a similar question when dealing with dynamic behavioral changes on my models. Playing around with irb, I found out that this works:

module Foo
   attr_accessor :bar
end
class Bar
   extends Foo
end
bar.bar = 'test' # 'test'
bar.bar # 'test'
# also works for instances of Bar!

So i would create an attribute called image_style that could be changed to whatever module you want to add, by using this code on Image initialization:

  def after_initialize
    if self.image_style?
       extend Object.const_get(image_style)
    else
       extend DefaultImageStyle
    end
  end

I just wonder if this works with paperclip since the after_initialize method may be called after paperclip does it's magic.. Worth a try though!

0
votes

Looking at the Paperclip source for Attachment it looks like the styles hash can take an object that responds to call so you may be able to do something like:

class Image < ActiveRecord::Base
  belongs_to :imageable, :polymorphic => true
  has_attached_file :file, :styles => lambda {|attachment| attachment.instance.imageable_type.constantize.image_styles }
end

Then in any model that has many images:

class Art < ActiveRecord::Base
  has_many :images, :as => :imageable

  def self.image_styles
    { :thumb => "150x150>", :normal => "492x600>" }
  end
end

Edit: Looks like someone else beat me to it :P.

-2
votes

have the same problem in a production/staging server...BUT not in my local environment. I use same rails/paperclip versions in all servers (2.3.2 and 2.2.6)