ユーザーを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_util.rb
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オブジェクト)が渡される)