attachment_fuプラグインで画像アップロード

前提

RMMagicをインストールしておく。

インストール

railsアプリケーションのトップでscript/pluginを実行してインストール

>ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu

今回の例

既存の商品モデル(product)の編集ページに対してその商品写真(photo)を加えるという例を作ってみる。
http://d.hatena.ne.jp/cuspos/20071110を参照させていただきました。

Model生成とmigrate

アップロードデータを扱うモデルを作成。
今回は商品(product)の商品画像(photo)のモデルを作成。

nyaago@kyonkyon: 501 $./script/generate model photo
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/photo.rb
      create  test/unit/photo_test.rb
      create  test/fixtures/photos.yml
      exists  db/migrate
      create  db/migrate/20090607125051_create_photos.rb

それから、migration。
Photoテーブルを生成する。各カラムは、fu_attachmentでのおきまりのよう。

class CreatePhotos < ActiveRecord::Migration
  def self.up
    create_table :photos do |t|
      t.column :parent_id,  :integer
      t.column :content_type, :string
      t.column :filename, :string    
      t.column :thumbnail, :string 
      t.column :size, :integer
      t.column :width, :integer
      t.column :height, :integer
      t.timestamps
    end
  end

  def self.down
    drop_table :photos
  end
end

それから、商品テーブル側に、photoへの参照をつけるmigrate

nyaago@kyonkyon: 502 $./script/generate migration mod_product
      exists  db/migrate
      create  db/migrate/20090607125553_mod_product.rb
class ModProduct < ActiveRecord::Migration
  def self.up
    add_column(:products, :photo_id, :integer, :null => true)
  end

  def self.down
     remove_column(:products, :photo_id)
  end
end
rake db:migrate

model

photoモデルの実装を行う。

class Photo < ActiveRecord::Base
  
  	has_attachment :content_type => :image, 
                   :storage => :file_system, 
                   :max_size => 1.megabyte,
                   :thumbnails => { :thumb => '100x100>', :small => '50x50>' }

    validates_as_attachment

    def full_filename(thumbnail = nil)
      file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
      File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
    end
end
    1. has_attachmentメソッドで画像の保存方法を指定。

今回、使っているのは以下のオプション

:content-type ファイルタイプ指定。今回は画像。他に何が指定できるのかは調べていないけど。
:storage ストレージタイプ。今回がファイルシステム。他に:db_file(db), :s3(amazon s3)がある。(プラグインのtechnoweenie/attachment_fu_/backends以下に対応するクラスが定義されている。)
:max_size ファイルの最大サイズ
:thumbnails これを指定すると、サムネイルを作ってくれる。

あと今回使ってないオプション

:resize_to リサイズして登録される。
      1. validates_as_attachmentメソッドにより、保存時のvalidationが実行されるようになる。
      1. full_filenameメソッドをオーバーライドすることにより、ファイルの保存場所を変更できる。今回、デフォルト実装のままだけど後で変更したくなりそうなので、ここの定義しているみている。


商品(product)モデル側も修正。

class Product < ActiveRecord::Base
  belongs_to :photo

  .. 省略 ..

商品からそれに関する写真を参照するためbelongs_to属性を指定。

views

以下、関係あるところだけを引用。

  <div>
    <% unless @product.photo.nil? then  %>
    <%= image_tag(@product.photo.public_filename()) %>
    <% end %>
    </div>
    <div>
    <label for="pthto">写真を追加:</label>
    <%= f.file_field :uploaded_data %>
    </div>
  
    1. モデルのpublic_filenameメソッドにより、画像へのurlが返されるので、その結果を使ってimageタグを作っている。
    2. file_fieldメソッドで ファイルアップロードタグを作っている。:uploaded_dataという名前を指定して作ってやるのが標準。そうすることにより。controller側ではPhoto.new(params[:photo] )としてmodelを作ってsaveするだけで、画像の保存までできる。

controller

参照元商品とこのphotoを保存するための定義を行う。

  # productの更新を行う。
  # Validationでエラーがあった場合は、再度入力ページ(edit)を表示する。
  def update
    begin
      if params[:id] and params[:id].to_i > 0 # for update
        product       = Product.find_by_id(params[:id])
        if product.nil? then
          redirect_to :controller => '/error', :action => 'index'
        end
        photo = prepare_photo_for_update(product)
        product.attributes = 
            params[:product].dup.delete_if { |key, value| key.to_sym == :uploaded_data }
      else          # for insert
        photo = prepare_photo_for_insert
        product = Product.new()
        product.attributes = 
            params[:product].dup.delete_if { |key, value| key.to_sym == :uploaded_data }
        product.photo_id = photo.id
      end
      # validate
      unless  photo.nil?
        unless photo.valid?
          prepare_for_edit(product)
          return render(:action => "edit")
        end
      end
      unless product.valid?
        prepare_for_edit(product)
        return render(:action => "edit")
      end
      # save
      Product.transaction {
        unless photo.nil?
          if(!photo.save)
            prepare_for_edit(product)
            return render(:action => "edit")
          end
          product.photo_id = photo.id
        end
        if(!product.save)
          prepare_for_edit(product)
          return render(:action => "edit")
        end
      }
    rescue => e
      p e.inspect
      logger.debug(e.inspect)
      return redirect_to_error(e.inspect)
    end
    
    #このあと省略(表示ページへのリダイレクトをする)
    
  end
  
  private
  
  # 商品挿入時のphotoモデル生成
  def prepare_photo_for_insert
    return nil if params[:product][:uploaded_data].blank?
    photo = Photo.new()
    # puts params[:product][:uploaded_data]
    photo.uploaded_data = params[:product][:uploaded_data]
    photo
  end

  # 商品変更時のphotoモデル生成
  def prepare_photo_for_update(product)
    photo = if product.photo_id then
        Photo.find_by_id(product.photo_id)
      else
        nil
      end
    if photo.nil?
      Photo.new(:uploaded_data => params[:product][:uploaded_data])
    else
      photo.uploaded_data = params[:product][:uploaded_data]
      photo
    end 
  end
  
  # 編集ページを開く場合の準備
  def prepare_for_edit(product)
    @product = product
    if 
      @photo = Photo.find_by_id(product.photo_id)
    else
      @photo = nil
    end
  end

上記処理についての備考。

    1. photoモデルのuploaded_dataにファイルパス(リクエストパラメータの:uploaded_data)を渡すことにより、モデルがvalidationや画像の保存を適切にしてくれるようになる。
    2. 複数モデルの更新なのでトランザクション処理をしている。ただし、ファイルシステム への画像の保存をしているので、それだけではうまく行かない。保存処理を行う前に別途validationをしている(根本的な解決ではないけど..)。
    3. リクエストパラメータからまとめてProductモデルに属性を渡すときは、photoモデルの属性は省いてやる必要がある。(delete_ifで省いている)。

課題

以下は、これから検討。

    1. 画像削除する。
    2. 保存確認ページを実装する場合はどうする?
    3. 商品に対する複数Photoの保存(has_many アソシエーション)。
    4. 一覧データ+画像ファイル群からバッチで登録する。
    5. 保存場所をアプリケーション以下とは別の場所(または別ホスト)に保存できるか?