Easy, named attachments with attachment_fu

ruby, code

I always seem to have models that have many attachments, but some of them are special. Like on this site I have a Site class that has a tout image, a before image, and an after image (for redesigns).

I wanted one polymorphic Attachment model to handle all of it.

I now use this module to make it easy on myself. It's a spin off the Advanced Rails Recipes file upload.

In the controller the @site.save becomes @site.save_with_attachments and @site.update becomes @site.update_with_attachments

In my model I add

1
2
3
4
class Site < ActiveRecord::Base
  include AttachmentUpload
  attachment_names :tout_img, :before_img, :after_img
end

In my sites/edit.html.erb I add

1
2
3
4
5
6
7
8
9
<%= image_tag @site.attachments.tout_img.public_filename(:thumb) if @site.attachments.tout_img %>
  <p>
    <b>Tout Pic</b><br />
    <%= f.file_field :uploaded_data_tout_img, :class => :text %>
  </p>
  <p>
    <%= label :site, :remove_tout, "Remove Photo", :class => "left" %>
    <%= f.check_box :remove_tout_img %>
  </p>

(For new.html.erb remove everything but the file_field.)

And then lib/attachment_upload.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
module AttachmentUpload

  # Add the attachment_names to the including model
  def self.included(base)        #:nodoc:
    base.extend AttachmentUpload
    @@class = base
  end

  # List the different named attacments
  # add the attr_accessor for each one
  # add the named association through the has_many :attachments
  def attachment_names(*names)
    @@attachment_names = names
    has_many :attachments, :as => :attachee, :dependent => :destroy do
      names.each do |name|        
        class_eval <<-EOS
        #{@@class}.send(:attr_accessor, :uploaded_data_#{name}, :remove_#{name})
        def #{name}(reload=false)
          @#{name} = nil if reload
          @#{name} ||= find(:first, :conditions => ["attachment_type = ?",'#{name}'])
        end
        EOS
      end
    end
  end

  # for create
  def save_with_attachments
    begin 
      @@attachment_names.each { |attachment| do_attachment(attachment) }
      save! 
    rescue 
      add_errors(@attachment)
    end 
  end

  # for update
  def update_with_attachments(params) 
    begin
      self.transaction do
        update_attributes(params)
        @@attachment_names.each { |attachment| do_attachment(attachment) }
        save!
      end 
    rescue 
      add_errors(@attachment)
    end 
  end

  # popuplates the Attachment.new
  def do_attachment(attachment_name)
    @attachment = Attachment.new
    if eval("remove_#{attachment_name} == '1' && self.attachments.#{attachment_name}")
      eval("self.attachments.#{attachment_name}.destroy")
    elsif eval("uploaded_data_#{attachment_name} && uploaded_data_#{attachment_name}.size > 0")   
      eval("self.attachments.#{attachment_name}.destroy if self.attachments.#{attachment_name}")
      @attachment.uploaded_data = eval("uploaded_data_#{attachment_name}")
      @attachment.attachment_type = attachment_name.to_s
      @attachment.thumbnails.clear 
      @attachment.save! 
      self.attachments << @attachment
    end
    @attachment
  end
  
  # Assigns Attachment errors to base
  def add_errors(attachment)
    if attachment.errors.on(:size) 
      errors.add_to_base("#{attachment.filename} file (#{attachment.size}) is too big (1MB max).") 
    end 
    if attachment.errors.on(:content_type) 
      errors.add_to_base("#{attachment.filename} content-type (#{attachment.content_type}) is not valid.") 
    end 
    false
  end
end

db/migrations/001createattachments.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create_table :attachments do |t|
      t.integer :position
      t.string :attachee_type
      t.integer :attachee_id
      t.string :attachment_type

      t.integer :size           # file size in bytes
      t.string  :content_type   # mime type, ex: application/mp3
      t.string  :filename       # sanitized filename
      t.integer :height         # in pixels
      t.integer :width          # in pixels
      t.integer :parent_id      # id of parent image (on the same table, a self-referencing foreign-key).
      t.string  :thumbnail      # the 'type' of thumbnail this at
      
      t.timestamps
    end

app/models/attachment.rb

1
2
3
4
5
6
7
8
9
10
class Attachment < ActiveRecord::Base
  belongs_to :attachee, :polymorphic => true
  has_attachment :content_type => ['application/pdf', :image],
                  :max_size => 3.megabyte,
                  :storage => :file_system, 
                  :path_prefix => 'public/attachments',
                  :resize_to => '800x800>',
                  :thumbnails => {:tout => '580>', :thumb => 'x150' }
  validates_as_attachment
end

I would love some fedback but I haven't had a chance to add comments yet...