モデルの追加
楽器(Instrumental)クラスを作成します。Title に belongs_to したモデルで、それ以外は並び順(sort_order)の属性を保持します。並び順は10置きに設定し、クラスメソッド renumber により、再整列できるようにします。この時、モデルのジェネレーターは以下のようになります。
r g model instrumental title_id:integer sort_order:integer
取り急ぎ spec/models/instrumental_spec.rb で作成のテストを書いてみます。前回の Title 同様、生成とバリデーションです。
require 'spec_helper' describe Instrumental do let(:instrumental) { FactoryGirl.build(:instrumental) } it '妥当なオブジェクトが生成されること' do expect(instrumental).to be_valid end [ :sort_order, :title_id ].each do |key| it "#{key} は空であってはならない" do instrumental[key] = "" expect(instrumental).not_to be_valid expect(instrumental.errors[key]).to be_present end end
このテストのために FactoryGirl が書きますが、今回は関連が絡んでくるので気をつける必要があります。spec/factories/instrumentals.rb は以下のようになります。title だけの行は Title の FactoryGirl へのリンクになります。
# Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do factory :instrumental do sort_order 1 title end end
app/models/instrumental.rb に実装を追加します。
class Instrumental < ActiveRecord::Base validates :sort_order, :title_id, presence:true belongs_to :title
また、app/models/title.rb に has_one を追加します。
has_one :instrumental
次にお互いを削除した時の挙動を考えます。楽器は今のところ削除可能で、削除した時にはタイトルから見えなくなります。逆にタイトルは楽器が登録されている時には削除できないようにすべきです。この条件をテストします。
it "削除したときに対応するタイトルの楽器のリンクが消えること" do instrumental.save title = instrumental.title expect(title.instrumental).to eq(instrumental) instrumental.destroy expect(title.instrumental(true)).to be_nil end it "楽器が所属するタイトルが削除できないこと" do instrumental.save title = instrumental.title expect { title.destroy }.not_to change(Title, :count) end
前者は特に何もする必要はありませんが、後者は destroy に対するバリデーションになります。先ほどの has_one の前に before_destroy が必要となります。記述順に注意する必要があるそうです(Railsでbefore_destroyを使うときの注意点参照)。
before_destroy do self.instrumental == nil end has_one :instrumental
次にタイトルに関するテストをします。楽器はタイトルへのリンクを保持するだけですので自分では表示ができません。そのため、name(is_ja) というメソッドを準備します。テストは以下のようになります。
it "name(flag) にてタイトルが取得できること" do expect(instrumental.name(true)).to eq('日本語 英語') expect(instrumental.name(false)).to eq('English Japanese') end end
name の実装ですが、これから作成する多くのクラスでも同じ処理になりますので、concerns にモジュールを作成し、それを include することにします。app/models/concerns/name.rb は以下のようになります。name という名前は安直でしょうか。
module Name def name(flag) title.name(flag) end end
楽器のメソッドにしたいので、app/models/instrumental.rb に include を追加します。
class Instrumental < ActiveRecord::Base include Name
とりあえず、モデルについては以上です。受け入れテストで必要なメソッド(renumber等)があるのですが、それはテストを書いてから追記します。
受け入れテスト
今回は一気に受け入れテストを作ってしまいます。シナリオは以下の通りです。
機能: 楽器に関する CRUD を確認する シナリオ: 楽器ページにアクセスする 前提 トップページを表示する もし "楽器" をクリックする ならば 画面に "楽器一覧" と表示されていること もし "English" をクリックする ならば 画面に "List of instrumentals" と表示されていること シナリオ: 運用環境では楽器追加ができない 前提 運用環境である かつ 楽器一覧を表示する ならば 画面に "楽器追加" と表示されていないこと シナリオ: 開発環境で新規楽器を作成する 前提 タイトルにテスト用データを登録する かつ タイトル一覧を表示する もし "ほ" をクリックする かつ id が "ボーカル" である行の "楽器追加" をクリックする ならば 画面に "楽器一覧" と表示されていること かつ id が "ボーカル" である行に "10" と表示されていること もし "English" をクリックする ならば id が "Vocal" である行に "10" と表示されていること もし タイトル一覧を表示する かつ "English" をクリックする かつ "HE" をクリックする かつ id が "Arrangement" である行の "Add an instrumental" をクリックする ならば id が "Arrangement" である行に "20" と表示されていること もし "Japanese" をクリックする ならば id が "編曲" である行に "20" と表示されていること シナリオ: 開発環境で楽器を編集する 前提 楽器にテスト用データを登録する もし 楽器一覧を表示する かつ id が "編曲" である行の "編集" をクリックする ならば 画面に "楽器編集 (編曲)" と表示されていること もし "並び順" に "5" と入力する かつ "更新する" ボタンをクリックする ならば 画面に "楽器一覧" と表示されていること かつ id が "編曲" である行に "10" と表示されていること かつ "English" をクリックする かつ id が "Arrangement" である行の "Edit" をクリックする ならば 画面に "Edit the instrumental (Arrangement)" と表示されていること かつ "Sort order" に "30" と入力する かつ "Update Instrumental" ボタンをクリックする ならば 画面に "List of instrumentals" と表示されていること かつ id が "Arrangement" である行に "20" と表示されていること シナリオ: パラメータ設定が間違えていたときのエラー確認(楽器編集) 前提 楽器にテスト用データを登録する もし 楽器一覧を表示する かつ id が "編曲" である行の "編集" をクリックする かつ "並び順" に "" と入力する かつ "更新する" ボタンをクリックする ならば 画面に "楽器一覧" と表示されていないこと かつ 画面に "並び順を入力してください。" と表示されていること もし 楽器一覧を表示する かつ "English" をクリックする かつ id が "Vocal" である行の "Edit" をクリックする かつ "Sort order" に "" と入力する かつ "Update Instrumental" ボタンをクリックする ならば 画面に "List of instrumentals" と表示されていないこと かつ 画面に "Sort order can't be blank" と表示されていること シナリオ: 開発環境で楽器を削除する 前提 楽器にテスト用データを登録する もし 楽器一覧を表示する かつ id が "ボーカル" である行の "削除" をクリックする ならば 画面に "楽器一覧" と表示されていること かつ 画面に "ボーカル" と表示されていないこと かつ id が "編曲" である行に "10" と表示されていること かつ "English" をクリックする かつ id が "Arrangement" である行の "Delete" をクリックする ならば 画面に "List of instrumentals" と表示されていること かつ 画面に "Arrangement" と表示されていないこと シナリオ: 楽器が登録されているときにタイトルをロックする 前提 楽器にテスト用データを登録する もし タイトル一覧を表示する かつ "ほ" をクリックする ならば id が "ボーカル" である行に "削除" と表示されていないこと もし "English" をクリックする かつ "HE" をクリックする ならば id が "Arrangement" である行に "削除" と表示されていないこと
タイトルとほぼ変わりませんが、以下の点を重点的に確認しています。
- タイトル一覧で楽器追加リンクをクリックするだけで、楽器を作成
- 並び順は自動的に 10 置きに採番
- 並び順を適当な値に設定すると並び順を再度整列して採番
- 削除した時も整列して採番
これらのテストを通過させるために、step を記述します。spec/acceptance/steps/titles_page_steps.rb に以下の step を追加します。
step 'タイトルにテスト用データを登録する' do [ %w(ボーカル Vocal ぼーかる), %w(編曲 Arrangement へんきょく) ].each do |(japanese, english, yomi)| Title.create(japanese:japanese, english:english, yomi:yomi) end end
また、spec/acceptance/steps/top_page_steps.rb に行の確認 step を追加します。
step 'id が :name である行に :string と表示されていること' do |name, string| within("//tr[@id='#{name}']") do expect(page).to have_content(string) end end step 'id が :name である行に :string と表示されていないこと' do |name, string| within("//tr[@id='#{name}']") do expect(page).not_to have_content(string) end end
さらに、spec/acceptance/steps/instrumentals_page_steps.rb に楽器用の step を新規作成します。step の中で send を呼ぶことで別の step を呼ぶことができます。
# encoding: utf-8 step '楽器一覧を表示する' do visit instrumentals_path(locate: :ja) end step '楽器にテスト用データを登録する' do send 'タイトルにテスト用データを登録する' %w(Vocal Arrangement).zip([10, 20]).each do |(english, sort_order)| Title.english_value_is(english).first.create_instrumental(sort_order:sort_order) end end
楽器のコントローラを作成します。
r g controller instrumentals
config/routes.rb も修正します。
resources :instrumentals
途中何度もテスト失敗・実装追加の繰り返しを行いますが、最終的なコントローラ(app/controllers/instrumentals_controller.rb)は以下のようになります。create が view から直接呼ばれるためにパラメータエラーを確認していない以外はほとんどタイトルと同じです。ただし、更新・削除時に並び順を renumber しています。
class InstrumentalsController < ApplicationController before_action :reject_production, except:[ :index, :show ] before_action :get_instrumental, only:[ :edit, :update, :destroy, :show ] def index @instrumentals = Instrumental.order_sort_order end def create @instrumental = Instrumental.create(instrumental_params) redirect_to instrumentals_path end def edit end def update if @instrumental.update(instrumental_params) Instrumental.renumber redirect_to instrumentals_path else render action: :edit end end def destroy @instrumental.destroy Instrumental.renumber redirect_to instrumentals_path end def get_instrumental @instrumental, @ids = get_objects_and_ids [ Instrumental ] end private :get_instrumental def instrumental_params params.require(:instrumental).permit(:sort_order, :title_id) end private :instrumental_params end
モデル(app/models/instrumental.rb)に足りないメソッドを追加します。
scope :order_sort_order, -> { order self.arel_table[:sort_order] } scope :english_value_is, -> v { joins(:title).merge(Title.english_value_is(v)) } def self.renumber self.order_sort_order.each_with_index do |obj, i| count = (i + 1) * 10 unless obj.sort_order == count obj.sort_order = count obj.save end end end def can_delete? true end
また、タイトルモデル(app/models/title.rb)にも足りないメソッドを追加します。
scope :english_value_is, -> v { where self.arel_table[:english].eq(v) }
また、楽器が存在しない時にはタイトルを削除できないようにするため、can_delete? を修正します。
def can_delete? instrumental ? false : true end
app/views/instrumentals/edit.html.haml はタイトルのものとほとんど一緒です。
%h1 #{t '.title'} (#{@instrumental.name(@is_ja)}) =render partial:'form'
また、app/views/instrumentals/_form.html.haml もほぼ一緒です。new がないので分ける必要はなかったかもしれません。
=form_for @instrumental do |f| =display_error(@instrumental) %table.table-bordered.table-striped -[ :sort_order ].each do |key| %tr %th=f.label key %td=f.text_field key %tr %td.text-center{colspan:2}=f.submit nil, class:"btn btn-default"
app/views/instrumentals/index.html.haml は並び順に全てを表示するだけで、ほぼタイトルと同じです。
%h1 #{t '.title'} %table.table-bordered.table-striped.table-hover %thead %tr -%w(.sort_order link.instrumentals).each do |k| %th=t k %th=t 'head.control' %tbody -@instrumentals.each do |instrumental| -name = instrumental.name(@is_ja) %tr{id:name} %td=instrumental.sort_order %td=name %td -if @is_dev .btn-group.btn-group-xs =link_to t('link.edit'), edit_instrumental_path(instrumental), class:"btn btn-default" =link_to t('link.destroy'), instrumental_path(instrumental), class:"btn btn-default", method: :delete, data:{ confirm:t('confirm.destroy') } if instrumental.can_delete?
ナビゲーションリンク(pp/views/shared/_top_link_bar.html.haml)も追加しておきます。
%li{active_from_class(InstrumentalsController)}=link_to t('link.instrumentals'), instrumentals_path
最後にタイトルのページを修正します。まず、表タイトルに楽器を追加します。
-%w(link.titles, link.instrumentals).each do |k|
表の中身の楽器列を追加します。開発環境では、create への直リンクを用意します。並び順は現在の最大値+10に自動設定します。一つもなかった場合には10です。タイトルに楽器が存在した場合、楽器を表示するリンクがありますが、この部分はまだ実装していません。
%td -if instrumental = title.instrumental .btn-group.btn-group-xs =link_to t('link.show'), instrumental, class:'btn btn-default' -elsif @is_dev .btn-group.btn-group-xs -max = Instrumental.maximum(:sort_order) || 0 =link_to t('instrumentals.new.title'), instrumentals_path(instrumental:{title_id:title.id, sort_order:max+10}), method: :post, class:'btn btn-default'
後はローカライズの文字列が設定されれば、テストは通過するはずです。
まとめ
関連が絡む部分で若干違う部分はありましたが、中身はほとんどタイトルの時と同じでしたね。今回は英語版も同時生成する目的があったため、config/locales/*/{ja,en}.yml を使いました。仮に日本語のサイトしか作らない場合でも、この仕組みを使った方が楽なのではないかと思いました。某システムではページごとに表記の揺らぎがあり、いろいろと迷惑をかけているのです。