SimpleForm のインストール
旧システムで最もトラブっていたのは、ダブルクリックによるデータ二重化問題であった。検索していたところ、http://qiita.com/kakipo/items/d3fb5595ccfa7250f3eb というページを見つけた。旧システムでは form もリンク形式で表示することで、ユーザの統一感を持たせる形にしていたが、今回のシステムでは副作用でデータ生成が入る部分については、disable_with を設定するようにする。この記事では、SimpleForm という gem の submit の拡張としての記事になっているので、SimpleForm を調べてみた。
通常の form_for では label と実際の入力は自分で記述する必要があった。simple_form ではこのあたりはモデルを参照して locale から適切に設定をしてくれるとのことである。ラベルの表記がなくなるとかなり実装の手間が減る。早速 Gemfile に simple_form を追加し、bundle install する。
# simple form gem 'simple_form', '~> 3.1.0'
その後、simple_form の generator を起動する。今回は Bootstrap を使っているので、--bootstrap オプションも記述する。
$ bin/rails g simple_form:install --bootstrap [Simple Form] Simple Form is not configured in the application and will use the default values. Use `rails generate simple_form:install` to generate the Simple Form configuration. create config/initializers/simple_form.rb create config/initializers/simple_form_bootstrap.rb exist config/locales create config/locales/simple_form.en.yml create lib/templates/haml/scaffold/_form.html.haml =============================================================================== Be sure to have a copy of the Bootstrap stylesheet available on your application, you can get it on http://getbootstrap.com/. Inside your views, use the 'simple_form_for' with one of the Bootstrap form classes, '.form-horizontal' or '.form-inline', as the following: = simple_form_for(@user, html: { class: 'form-horizontal' }) do |form| ===============================================================================
submit_tag の拡張
SimpleForm がインストールできたので、先の記事の submit_tag の拡張を行う。これは、data:{ disable_with:’xxx’ } を毎回記述しなくてよいようにするものである。config/initializers/simple_form.rb に以下の module を追記する。
# @see http://qiita.com/kakipo/items/d3fb5595ccfa7250f3eb # @note form の二重送信を防ぐ SimpleForm::FormBuilder.class_eval do def submit_with_override(value=nil, options={}) value, options = nil, value if value.is_a?(Hash) data_disable_with = { disable_with: '送信中...' } options[:data] = data_disable_with.merge(options[:data] || {}) submit_without_override(value, options) end alias_method_chain :submit, :override end
index のテストを記述
現在の teacher/years はタイトルしか表示していないので、取得した年度一覧を表示する。まず、spec/controllers/teacher/years_controller.rb の spec を修正する。
let(:year) { FactoryGirl.build(:year) } before do login_user_as :user_skyoumu, controller end describe "GET index" do before do @years = [ double(Year, year: 2014), double(Year) ] expect(Year).to receive(:order_year_desc) { @years } get :index end it { expect(response).to have_http_status(:success) } it("should have @years") { expect(assigns[:years]).to eq(@years) } it("should have a correct next_year instance") { expect(assigns[:next_year]).to eq(2015) } end
index の controller を記述
このテストを通すために、app/controllers/teacher/years_controller.rb を修正する。
def index @navis = [ '年度変更' ] @years = Year.order_year_desc @next_year = @years.first.year + 1 end
index の view を追加
一覧を描画するために app/views/teacher/years/index.html.haml にテーブルを追加する。現在設定されている年度は、info というクラスを設定することで目立たせている。
%h1 年度一覧 =simple_form_for :year, url: teacher_years_path do |f| %table.table.table-striped.table-hover.table-bordered %tr %th= t_ar 'Year.wareki' %th= t_ar 'Year.year' %th= t_ar 'Year.default_year' %tr %td{colspan: 3} =f.input :year, as: :hidden, input_html: { value: @next_year } =f.input :default_year, as: :hidden, input_html: { value: false } =f.button :submit, "#{Year.wareki_nendo(@next_year)}を作成", class: 'btn-block' - @years.each do |year| %tr{ class_info_or_nil(@now_year == year)} %td= year.wareki_nendo %td= year.year %td= mb year.default_year
helper を追加
ヘッダ部の t_ar は locale から翻訳文字列を取得するメソッドである。また、class_info_or_nil はフラグに従って、CSS class に ‘info’ を設定するメソッドである。mb はフラグに従って、○または×を出力するメソッドである。これらはすべてヘルパとして app/helpers/applicaiton_helper.rb に追記する。
# @param [String] str locale キーワード # @return [String] 翻訳後の文字列 # @see http://rails3try.blogspot.jp/2012/01/rails3-i18n.html?m=1 def t_ar(str) model_str, atr = str.split(/\./) model = model_str.constantize if atr return model.human_attribute_name(atr) else return model.model_name.human end end # @param [Boolean] flag info class を設定するかどうか # @return [Hash] flag が true なら { class: 'info' }、false なら {} def class_info_or_nil(flag) flag ? { class: 'info' } : {} end # @param [Boolean] flag true or false # @return [String] true なら○、false なら× def mb(flag) flag ? '○' : '×' end
create のテストを追加
なお、表示ができたので受入テストも進み、以下のようなエラーとなる。
Failure/Error: もし "平成27年度を作成"ボタンをクリックする AbstractController::ActionNotFound: The action 'create' could not be found for Teacher::YearsController
create メソッドを作成するために、spec/controllers/teacher/years_controller_spec.rb にテストを記述する。コントローラのテストでは基本的に mock と stub を使い、user 以外のデータベースは使わない。ここでのよくハマるポイントは、stub に渡すパラメータハッシュのキー。url を介して処理されるので、文字列になってしまう。stub ではシンボルだと一致しないことになる。
describe "POST create" do before do @params = { 'year' => '2015', 'default_year' => false } @year = double(Year, wareki_nendo: '平成27年度') expect(Year).to receive(:new).with(@params) { @year } end context "when year.save is true" do before do expect(@year).to receive(:save) { true } post :create, year: @params end it { expect(response).to redirect_to(teacher_years_path) } it('should have a correct notice message') { expect(flash[:notice]).to eq('平成27年度を作成しました。') } end context "when year.save is false" do before do expect(@year).to receive(:save) { false } post :create, year: @params end it { expect(response).to redirect_to(teacher_years_path) } it('should have a correct alert message') { expect(flash[:alert]).to eq('平成27年度はすでに存在します。') } end end
create の実装を追加
このテストを通過させるための実装は以下のようになる。Rails 4 以降では strong parameters により params のクリーニングを行う必要がある。今回は year_params という private メソッドを使用している。
def create year = Year.new(year_params) if year.save redirect_to teacher_years_path, notice: "#{year.wareki_nendo}を作成しました。" else redirect_to teacher_years_path, alert: "#{year.wareki_nendo}はすでに存在します。" end end private def year_params params.require(:year).permit(:year, :default_year) end
これでコントローラのテストは通過した。
ブラウザでのテスト
確認のためにブラウザで実行したところ、「平成30年度はすでに存在します。」というエラーが出てしまった。確認したところ、app/models/year.rb の validates の記述が間違っていたことを確認した。default_year は boolean なので、presence:true では検証できないことを失念していた。旧システムと同様に inclusion で validates するように修正した。
validates :year, presence:true, uniqueness:true validates :default_year, inclusion: { in: [true, false] }
この結果、ブラウザでも正しく年度が追加できた。
初期データの巻き戻し
開発版データベースの Year テーブルに大量に年度を作成してしまったので、db:seed で元に戻すことにする。現在は、テーブルが空の場合だけデータ投入をしていたが、ファイルの日付が最近のものについては、読み込みをするように変更する。db/seeds.rb の該当部を以下のように修正する。
recent = File::stat(fname).mtime > (DateTime.now - 300.0/86400) if storeClass.first == nil || recent
現在時刻より5分以内に更新されたファイルである場合には、テーブルが空でなくても読み込みを実施するようになった。テストで years.sql の時刻を更新し、db:seed を実行してみる。18個のデータが削除され、17個のデータが投入されており、正しく動作していることがわかる。
$ touch dump/years.sql; bin/rake db:seed ### load years DELETE 18 COPY 17 18
長くなったので今日はここまで。
written by iHatenaSync