タイトルコントローラの作成(2)

タイトル追加部分の作成

  シナリオ: 開発環境で新規タイトルを作成する
    前提 タイトル一覧を表示する
    もし "[タイトル追加]" をクリックする
    ならば 画面に "タイトル追加" と表示されていること
    もし "日本語""森高 千里" と入力する
    かつ "英語""Chisato Moritaka" と入力する
    かつ "読み""もりたか ちさと" と入力する
    かつ "登録する" ボタンをクリックする
    ならば 画面に "タイトル一覧" と表示されていること
    かつ 画面に "森高 千里" と表示されていること

「The action 'new' could not be found for TitlesController」と言われているので、早速、app/controllers/titles_controller.rb に new メソッドを作成します。

  def new
    @title = Title.new
  end

続いて、app/views/titles/new.html.haml を記述します。form は edit と共有したいので partial で読み込みます。

%h1=t '.title'

=render partial:'form'

partial で読み込む app/views/titles/_form.html.haml は以下のようになります。

=form_for @title do |f|
  %table
    -[ :japanese, :english, :yomi ].each do |key|
      %tr
        %th=f.label key
        %td=f.text_field key
    %tr
      %td{colspan:2}=f.submit

足りない steps を追加します。共通部品なので top_page_steps.rb に登録しておきます。

step ':button ボタンをクリックする' do |name|
  click_button name
end

step ':label に :value と入力する' do |label, value|
  fill_in label, with:value
end 

Guard の結果は以下のようになります。ローカライズがされていないために、ElementNotFound になってしまっています。

Capybara::ElementNotFound:
       Unable to find field "日本語"

ローカライズ設定を行います。今回は、form の属性は models の locales から自動的に読み込まれるので、models の locales を設定します。まずはフォルダを作成します。

mkdir -p config/locales/models

次に、config/locales/models/ja.yml に title model 用の locale を追加します。

ja:
  activerecord:
    models:
      title: タイトル
    attributes:
      title:
        japanese: 日本語
        english: 英語
        yomi: 読み

同様に en.yml にも追加します。

ja:
  activerecord:
    models:
      title: Title
    attributes:
      title:
        japanese: Japanese
        english: English
        yomi: Yomi

この結果、「登録する」ボタンが押されるところまで実行され、create が呼ばれるため、以下のようなエラーとなります。

     Failure/Error: もし "[タイトル追加]" をクリックする
     AbstractController::ActionNotFound:
       The action 'create' could not be found for TitlesController

テストを通過させるため、app/controllers/title_controller.rb に create も作成します。Rails 4 なので、strong_parameters の設定も必要となります。

  def create
    @title = Title.new(title_params)
    if @title.save
      redirect_to titles_path(head:@title.head)
    else
      render action: :new
    end
  end

  def title_params
    params.require(:title).permit(:japanese, :english, :yomi)
  end
  private :title_params

title オブジェクトの作成に成功した場合、その頭文字を含むページを表示したいと思います。濁点・半濁点を共有するという目的のため、頭文字での絞り込みは yomi_suuji で行います。まず、実装の前に spec/models/title_spec.rb にテストを記載します。

  it "head にて yomi_suuji の先頭二文字が取得できること" do
    title.valid?
    expect(title.head).to eq('41')
  end

実装は yomi_suuji の先頭を取るだけなので簡単に記載できます。yomi_suuji が nil になることはないので、エラーチェックの必要はありません。

  def head
    yomi_suuji[0..1]
  end

今回の場合、title オブジェクト作成に成功しているので、titles_path にリダイレクトされるのですが、肝心の登録したものが表示されていないため、Guard はエラーとなっています。データの取得および表示を実装します。app/controllers/titles_controller.rb の index の中身を記述します。

  def index
    @head = params[:head]
    @head = "00" unless /^\d\d$/ =~ @head
    @titles = Title.head_value_is(@head).order_yomi
  end 

データ取得用の scope を app/model/title.rb に追加します。

  scope :head_value_is, -> v { where self.arel_table[:yomi_suuji].matches("#{v}%") }                                                              
  scope :order_yomi, -> { order self.arel_table[:yomi_suuji] }

取得した @titles を app/views/titles/index.html.haml にて表示します。

%table
  %tr
    -%w(link.titles).each do |k|
      %th=t k
  -@titles.each do |title|
    %tr
      %td=title.name(@is_ja)

model の name メソッドは true の時に日本語、false の時に英語を表示します。まず、spec/models/title_spec.rb にテストを書きます。

  it "name(flag) にて対応する言語の文字列が得られること" do
    expect(title.name(true)).to eq('日本語 英語')
    expect(title.name(false)).to eq('English Japanese')
  end

app/models/title.rb に name の実装を記述します。

  def name(flag)
    flag ? japanese : english
  end

これでテストは通過します。

他の頭文字への移動

現在、他の頭文字ページへのリンクが表示されていませんので、その部分を実装します。さきほどのシナリオに以下の文を追記します。

    もし "[タイトル追加]" をクリックする
    かつ "日本語""斉藤 英夫" と入力する
    かつ "英語""Hideo Saito" と入力する
    かつ "読み""さいとう ひでお" と入力する
    かつ "登録する" ボタンをクリックする
    ならば 画面に "斉藤 英夫" と表示されていること
    もし "[も]" をクリックする
    ならば 画面に "斉藤 英夫" と表示されていないこと
    かつ 画面に "森高 千里" と表示されていること

まず、頭文字の一覧を定数として app/controllers/application_controller.rb に用意しておきます。

  include YomiSuuji
  JHeads = %w(あ い う え お か き く け こ さ し す せ そ た ち つ て と な に ぬ ね の は ひ ふ へ ほ ま み む め も や ゆ よ ら り る れ ろ わ を)
  EHeads = %w(A I U E O KA KI KU KE KO SA SHI SU SE SO TA CHI TSU TE TO NA NI NU NE NO HA HI FU HE HO MA MI MU ME MO YA YU YO RA RI RU RE RO WA WO)

app/controllers/titles_controller.rb にて、言語に従ってこれらのヘッダを取得します。また、リンク用文字列も計算しておきます。

  def index
    @head = params[:head]
    @head = "00" unless /^\d\d$/ =~ @head
    @titles = Title.head_value_is(@head).order_yomi
    @heads = @is_ja ? JHeads : EHeads
    @lhead_hash = @heads.zip(JHeads.map { |fl| convert_yomi_suuji(fl).slice(0..1) }).to_h
  end

頭文字は数が多いので 2 段で表示することにします。Rails の Array 拡張である in_groups_of を使ってみます。

%table.bordered         
  %caption=t 'head.first_letter'
  -@heads.in_groups_of((@heads.count+1)/2.0).each do |array|
    %tr
      -array.each do |fl|
        %td
          =link_to lh(fl), titles_path(head:@lhead_hash[fl]) if fl

この結果、テストが無事に通過することになります。

運用環境におけるエラー確認

運用環境では new のページへのリンクは表示されませんが、Rails を知っている人なら URL を直接指定してしまう可能性があります。運用環境ではアクセスできないようにしておきます。まず、spec/acceptance/titles_page.feature にテストを書いてみます。

  シナリオ: 運用環境で直接タイトル追加ページにアクセスさせない
    前提 運用環境である
    もし タイトル追加ページを表示する
    ならば 画面に "森高千里データベース" と表示されていること

spec/acceptance/steps/titles_page_steps.rb に step を追加します。

step 'タイトル追加ページを表示する' do
  visit new_title_path
end

現在は、何の制約も付けていないので、Guard でエラーになっています。production 環境において許可がないページへアクセスした場合には、root_path にリダイレクトするように設定します。app/controllers/application_controller.rb に reject_production メソッドを追加します。

  def reject_production
    if @is_dev
      true
    else
      redirect_to root_path
    end
  end

続いて、app/controllers/titles_controller.rb に before_action を追加します。

  before_action :reject_production, except:[ :index, :show ]

これで production 環境においては new のページにアクセスできなくなり、テストが通過します。

パラメータ設定ミスにおけるエラー確認

次にパラメータが正しく設定されなかった場合のテストを追加します。

  シナリオ: パラメータ設定が間違えていたときのエラー確認
    前提 タイトル追加ページを表示する
    もし "日本語""" と入力する
    かつ "英語""Chisato Moritaka" と入力する
    かつ "読み""もりたか ちさと" と入力する
    かつ "登録する" ボタンをクリックする
    ならば 画面に "タイトル一覧" と表示されていないこと
    かつ 画面に "日本語を入力してください。" と表示されていること
    もし "日本語""森高 千里" と入力する
    かつ "英語""" と入力する
    かつ "登録する" ボタンをクリックする
    ならば 画面に "タイトル一覧" と表示されていないこと
    かつ 画面に "英語を入力してください。" と表示されていること
    もし "英語""Chisato Moritaka" と入力する
    かつ "読み""" と入力する
    かつ "登録する" ボタンをクリックする
    ならば 画面に "タイトル一覧" と表示されていないこと
    かつ 画面に "読みを入力してください。" と表示されていること
    かつ "読み""ABC" と入力する
    かつ "登録する" ボタンをクリックする
    ならば 画面に "タイトル一覧" と表示されていないこと
    かつ 画面に "読みは不正な値です。" と表示されていること

現在、エラーの表示がなされていないため、このテストは通過しません。app/views/titles/_form.html.haml にエラー表示部を追加します。

=form_for @title do |f|
  =display_error(@title)

display_error メソッドは、app/helpers/application_helper.rb に記載します。一つの属性に対して複数のエラーが表示されることがありますが、その場合には先頭の一つのみを表示するようにしています。

  # model エラー表示
  def display_error(model)
    if model.errors.any?
      error_hash = model.errors.map { |attr, msg| [attr, model.errors.full_message(attr, msg)] }.reverse.to_h
      render inline:<<-HAML, type: :haml, locals:{error_hash:error_hash}
#error_message                                   
  %ul                                            
    =list_of error_hash.keys.reverse.each do |key|
      =error_hash[key]
HAML             
    end
  end

これで、エラー表示が行われるようになり、先のテストが通過するようになります。なお、エラーメッセージを設定した覚えがないように思いますが、実は config/locales/ja.yml にテンプレートとして用意されたものが使用されています。

編集・削除までいけなかったので、それはまた後日。