CSVアップロード

ユーザーをCSVアップロードで登録する機能を実装してもたのでメモ。コントローラのコードが長かったり、メソッドの名前があまり良くないので、もっといい方法を見つけたら追記していきたい(*´艸`)

View

まずは簡単なビューから

  • csv_upload.rhtml
<% @title="ユーザーのCSV一括登録" -%>
アップロードの雛形を使う場合、事前にヘルプを参照して下さい。<br>
<% form_tag( {:action => "upload"},{ :multipart => true }) do -%>
<p>アップロードファイル<span style="color:red;">(必須)</span><br>
  <%= file_field_tag(:file) %>
</p>
<%= submit_tag("アップロード") %>
<% end -%>
<br>
<%= link_to "アップロードファイルの雛形","../../user/csv_up_format.csv" %> 
<%= link_to "一括登録のヘルプ",:action => "csv_upload_help" %>

ファイルを送るので、{:multipart => true}を指定する。また、雛形をpublic/user/ディレクトリにおいて置き、そのファイルをユーザーがダウンロードできるようにする。

Controller

次にコントローラ。これがあまり綺麗じゃないヾ(`д´)ノシ

  • user_controller.rb
require 'csv'

def upload  
  @users = []
  file = params[:file]
   
  if CSVUtil.valid_data_from_file?(file)
    CSV::Reader.parse(file) do |row|
      @users << User.new_by_array(row.to_a)
    end

    invalid_rows = []
    @users.each_with_index do |user, index|
      invalid_rows << i if user.invalid?
    end
     
    if invalid_rows.empty?
      @users.each{|user| user.save}
      flash[:notice] = "#{user.size}件のデータが登録されました"
      redirect_to :action => "list"
    else
      render_with_flash("csv_upload", <<-MSG.chomp)
#{invalid_rows.join(", ")}行目のデータが不正です。最初からやり直して下さい。
MSG
    end
  else
    render_with_flash("csv_upload","CSVファイルが空か、指定されたファイルが存在しません")
  end      
end  

トランザクションを使って以下のように書いてもよかったのですが、不正な行を一回ですべて表示したかったのでこのようにしました。

  • upload(transactionを使った版)
def upload
  @users = []
  line_count = 0
  file = params[:file]
  
  begin  
    if CSVUtil.valid_data_from_file?(file)
      Interview.transaction do
        CSV::Reader.parse(file) do |row|
          user = User.new_by_array(row.to_a)
          raise unless user.save
          @users << user
          line_count += 1
        end
      end
     
      flash[:notice] = "#{line_count}件のデータが登録されました"
      redirect_to :action => "list"
    else
      render_with_flash("csv_upload","CSVファイルが空か、指定されたファイルが存在しません")
    end      
  rescue => e
    render_with_flash("csv_upload", <<-MSG.chomp)
#{line_count + 1}行目のデータが不正です。最初からやり直して下さい。
MSG
  end
end

model

そしてモデル

  • user.rb
class << self
  def new_by_array(arr)
    arr.map! do |elem|  
      NKF::nkf('-S -w',elem) if elem
    end
      
    self.new(
              :family_name => arr[0], 
              :first_name => arr[1],
              :phone => arr[2],
              :address => arr[3]  
            )
  end
end

ライブラリ

CSVを扱うためのライブラリ。libディレクトリ下に置く。

class CSVUtil
  class << self
    def valid_data_from_file?(stream_data)
      if stream_data.respond_to?(:original_filename)
        (!stream_data.eof?) && (File.extname(stream_data.original_filename) == ".csv") 
      else
        false
      end
    end
  end
end

アップロードされたファイル(の中身)が存在するか、そのファイルはCSVファイルかを調べている。

テスト

  • user_controller_test.rb
def test_upload
  previous_count = User.count
  post :upload,:file => upload_file(File.dirname(__FILE__) + '/../../upload_files/test_upload.csv')
  assert_redirected_to :action => "list"
  assert_equal 2,assigns(:users).size
  assert_equal previous_count + 2,User.count
  satou = assigns(:users)[0]
  suzuki = assigns(:users)[1]
   
  assert_equal "佐藤", satou.family_name
  assert_equal "太郎", satou.first_name
  assert_equal "09012345678", satou.phone
  assert_equal "satou@test.com", satou.address
  assert_equal "鈴木", suzuki.family_name
  assert_equal "栄子", suzuki.first_name
  assert_equal "08011111111", suzuki.phone
  assert_equal "suzuki@test.com", suzuki.address
end

private
def upload_file(filename)
  if File.exist?(filename)
    f = File.open(filename,"r")
      
    (class << f; self; end).class_eval do
      define_method(:original_filename){File.basename(filename)}
    end
    
    f
  else
    StringIO.new
  end
end
  • test_upload.csv
佐藤,太郎,09012345678,sato@test.com
鈴木,栄子,08011111111,suzuki@test.com

test/upload_filesディレクトリ下にテスト用のCSVファイルを置き、そこからデータを読み込んでテストを行う。
upload_fileメソッドは、ファイルをopenし、そのファイルのデータを返す。実際に、アップロードされたファイルは、データとしてparams[:file]に入り、コントローラに渡されるので、テストでもこれをシミュレートしている。
ファイルが存在しない場合に、StringIOオブジェクトを返しているのもそのため。実際に、file_fieldに存在しないファイルのパスを入れると、空のStringIOオブジェクトがコントローラに渡される。(尚、パスでなく、単なる文字列を入れた場合は文字列(Stringオブジェクト)が渡される)