CSVサーバープロジェクトの開始
解決したい課題
会社には月次決算がつきものだ。決算では、エクセルなどの表計算ソフトで各種資料を作成する。管理部門が作成する資料は、部・課・係・商品コード別等々の組織ごとに集計した一覧表の形式であることが多い。作成した資料は、各部署に配布しなければならない。しかし、今時の会社である。以前のように、全社の合計が載った資料を全ての人に配布すると、上層部からクレームが付く。上層部からは、各役職の担当組織の分だけ配布するようにとの指示。しょうがないので、その指示通り配布しようとするが、これが意外と大変で、毎月の大仕事になってしまう。
まず、全社の組織別の数字が表示された一覧表から、配布する単位の組織の分だけコピーして、別ファイルを作成する。それを各部署の担当者宛にメールの添付ファイルとして送信する。文章で書くとこれだけだが、以下のような大変さが毎月ずっと続くのだ...。
- 配布先の組織が20あれば、20ファイルに分ける必要がある。
- 宛先と添付するファイルを間違えないように、十分な注意が必要だ。
- 人事異動は頻繁にあり、メールの宛先の管理にも手間がかかる。
- 資料は1つだけではない。配布する資料の数だけ、上記の作業負担が増えることになる。
- 資料に訂正があれば、再送することもある。
そこで、Railsの出番である。配布する資料をCSVファイルとして書き出し、それをCSVサーバーにアップロードする。各ユーザーはログインすると、自分の担当組織の部分だけ、各種資料を閲覧することが出来る。そんなwebアプリケーションを作ってみたい。csv_serverプロジェクトの始まり。果たして望む通りのものが出来るかどうか...。
-
-
- 多くの会社では、上記のような資料配布を、どのように対応しているのだろうか?苦労最小限の方法が知りたい...。それとも大した苦労じゃない?
-
CSVファイルをアップロードして、閲覧する。
前回まで、CSVファイルを取り扱うクラスを作成して、試行錯誤してみたが、やはりRailsで開発するなら、データベースに取り込んでしまった方が良いと感じた。その方が、手間もかからないし、高機能なActiveRecordがいろいろなことを助けてくれる。大まかな処理の流れは以下のようにする予定。まだ、ユーザーごとの閲覧制限はない。
CSVファイルを管理するCSVモデル、ビュー、コントローラー
モデルの作成
csvモデルには、以下の機能を追加する予定。
- アップロードしたファイルを元に、データベースにファイル名と同じテーブルを追加する。
- 追加したテーブルに、CSVファイルの内容をインポートする。
script/generate model csv
csvsテーブルを作成
- マイグレート
- db/migrate/001_create_csvs.rb
class CreateCsvs < ActiveRecord::Migration def self.up create_table :csvs do |t| t.column :file_name, :string t.column :file_comment, :string t.column :file_size, :integer t.column :created_at, :datetime t.column :updated_at, :datetime end end def self.down drop_table :csvs end end
scaffoldの実行
script/generate scaffold csv
csvモデルをコーディング
- モデル
- app/models/csv.rb
- CSVファイルをアップロードした時に、テーブルを追加して、データベースにインポートする処理を行う。
class Csv < ActiveRecord::Base # ファイル名がテーブル名になるので、重複チェックを行う。 validates_uniqueness_of :file_name # アップロードするファイルを指定しないと、エラーにする。 def validate errors.add(:file_name, "アップロードするファイルを指定してください。") if @file.nil? || @file.size == 0 end # Tempfileオブジェクトをインスタンス変数に保存する。 # new()や、update_attributes()の時、呼び出される。 def tempfile=(file) return if file.nil? || file.size == 0 @file = file self.file_name = file.original_filename self.file_size = file.size # file.read.sizeとやってしまうと、29行目で@file.readした時、なぜかnilが返る。 # 1回アクセスすると、そのデータは消えてしまう? end # テーブルを追加する。 def create_csv_table execute_sql("create table #{table_name} (#{csv_columns} PRIMARY KEY (id));") end # ファイル名の拡張子の手前までをテーブル名として返す。 def table_name File.basename(self.file_name, ".*") end # CSVファイルの1行目をフィールド名として返す。 # 処理の過程で、以下3つの処理も同時に実行する。 # - 文字コードの変換や余分な空白や特殊文字を整形する処理。 # - idフィールドの追加処理。 # - データ部分をpublic/temp.csvファイルとして保存する処理。 def csv_columns # 「"」を取り除いて、文字コードをUTF-8に変換して、改行で区切った配列を返す。 str = @file.read.gsub(/"+/, '') str = NKF.nkf('-w', str) lines = str.split("\n") # 配列の先頭を取り出して、カンマで区切った配列にして、余分な空白を削除する。 columns = lines.shift.split(",").collect{|n| n.strip} # id列が存在しなければ追加する。 if columns.index("id").nil? columns.unshift("id") id = 0 lines.collect! {|line| id +=1; "#{id},#{line}"} end # インポート用のファイルを保存する。 File.delete("public/temp.csv") rescue nil File.open("public/temp.csv", "wb") do |f| f.write(lines.join("\n")) end # フィールド名 string, ... の書式で返す。 columns.inject("") do |result, column| result << "#{column} string, " end end # csvデータをインポートする。 # インポートを高速化するため、sqlite3のインポート命令を、直接コマンド実行する。 def import_csv env = ENV['RAILS_ENV'] || 'development' system("sqlite3", "-separator", ",", "db/#{env}.sqlite3", ".import public/temp.csv #{table_name}") end # テーブルを削除する。 def drop_csv_table execute_sql("drop table #{table_name}") end # テーブルの追加、削除のSQLを実行する。 def execute_sql(sql) env = ENV['RAILS_ENV'] || 'development' db = SQLite3::Database.new("db/#{env}.sqlite3") db.execute(sql) db.close end end
csvsコントローラーの修正
- コントローラー
- app/controllers/csvs_controller.rb
class CsvsController < ApplicationController ...(途中省略)... def create @csv = Csv.new(params[:csv]) if @csv.save @csv.create_csv_table @csv.import_csv flash[:notice] = 'Csv was successfully created.' redirect_to :action => 'list' else render :action => 'new' end end ...(途中省略)... def destroy @csv = Csv.find(params[:id]).destroy @csv.drop_csv_table redirect_to :action => 'list' end ...(途中省略)... end
-
-
- オレンジ色の部分を追記した。
-
csvビューの修正
- 新規アップロード
- app/views/csvs/new.rhtml
- start_form_tagに、:multipart => trueを追加した。ファイルをアップロードするためのおまじない。忘れるとアップロードできない。
<h1>New csv</h1> <%= start_form_tag({:action => 'create'}, :multipart => true) %> <%= render :partial => 'form' %> <%= submit_tag "Create" %> <%= end_form_tag %> <%= link_to 'Back', :action => 'list' %>
- 入力フォーム
- app/views/csvs/_form.rhtml
<%= error_messages_for 'csv' %> <!--[form:csv]--> <p><label for="csv_tempfile">File</label><br/> <%= file_field 'csv', 'tempfile' %></p> <p><label for="csv_file_comment">File comment</label><br/> <%= text_field 'csv', 'file_comment' %></p> <!--[eoform:csv]-->
- アップロードしたCSVファイルのリスト表示
- app/views/csvs/list.rhtml
<h1>Listing csvs</h1> <table> <tr> <% for column in Csv.content_columns %> <th><%= column.human_name %></th> <% end %> </tr> <%= render :partial => 'listd', :collection => @csvs %> </table> <%= link_to 'Previous page', { :page => @csv_pages.current.previous } if @csv_pages.current.previous %> <%= link_to 'Next page', { :page => @csv_pages.current.next } if @csv_pages.current.next %> <br /> <%= link_to 'New csv', :action => 'new' %>
- リスト表示のデータ部分を描画
- app/views/csvs/_listd.rhtml
- ファイル名のリンクは、displaysコントローラーのlistメソッドを呼び出している。
- この時、パラメーターとしてテーブル名も送信している。
- displaysコントローラーでは、このテーブル名を見て、参照するテーブルを切り替えるようにした。
- "12345678".gsub(/(\d)(?=(\d\d\d)+(?!\d))/, '\1,')を実行すると、"12,345,678"が返ってくる。
- つまり、文字列から数字を発見して、3桁区切りでカンマを入れてくれる正規表現だ。(ちゃんと理解できていないが...。)
- 参考ページ:Rubyのある風景 - Regexp Lookahead
<!--[:]--> <tr> <td><%= link_to h(listd.file_name), :controller => 'displays', :action=>'list', :table => listd.table_name %></td> <td><%=h listd.file_comment %></td> <td></td> <td align="right"><%=h listd.file_size.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, '\1,') %> B</td> <td><%=h listd.created_at.strftime('%Y-%m-%d %H:%M:%S') %></td> <td><%=h listd.updated_at.strftime('%Y-%m-%d %H:%M:%S') %></td> <td align="center"><%= link_to 'Show', :action => 'show', :id => listd %></td> <td align="center"><%= link_to 'Edit', :action => 'edit', :id => listd %></td> <td align="center"><%= link_to 'Destroy', { :action => 'destroy', :id => listd }, :confirm => 'Are you sure?', :post => true %></td> </tr> <!--[:]-->
アップロードされた内容を閲覧するDisplayモデル、ビュー、コントローラー
モデルの作成
displayモデルはに、以下の機能を追加する予定。
- アップロードされた内容を保持しているテーブルを、ajax_scaffoldをベースに参照する。
- Displayモデル1つで、複数のテーブルを切り替えて、内容を表示する。
script/generate model display
displaysテーブルを作成
- マイグレート
- db/migrate/002_create_displays.rb
- これから追加されるテーブルを参照するので、ここで追加するテーブルは不要だが、この後のscaffoldを実行するため、便宜的に追加しておいた。
class CreateDisplays < ActiveRecord::Migration def self.up create_table :displays do |t| t.column :name, :string end end def self.down drop_table :displays end end
ajax_scaffoldの実行
- まだ、ajax_scaffold_generatorをインストールしていない場合は、以下を実行しておく。
gem install ajax_scaffold_generator
- あとは通常のscaffoldと同じように実行した。
script/generate ajax_scaffold display
displaysコントローラーの修正
- コントローラー
- app/controllers/displays_controller.rb
- 参照するテーブルを切り替える処理を追加した。
- 1ページの表示件数を50件にした。
class DisplaysController < ApplicationController include AjaxScaffold::Controller # 全ての処理に先立って、select_tableメソッドで参照するテーブルを選択する。 before_filter :select_table after_filter :clear_flashes before_filter :update_params_filter # 1ページの表示件数を設定 def default_per_page 50 end # 渡されたtableパラメーターを、テーブル名としてセットすれば、複数のテーブルを切り替えて参照できる。 def select_table Display.set_table_name params[:table] || 'displays' end ...(以下省略)...
-
-
- オレンジ色の部分を追記した。
-
以上で、CSVファイルをアップロードして、その内容を表示できるようになった。まだ、ajax_scaffold側の機能は完全に利用できないが、今後修正していく予定。