【Ruby】チュートリアルのform_forをform_withで書き換え (おまけ:capybaraでのテスト)
Ruby5.1前まではフォームを生成してくれるメソッドとして、form_for
とform_tag
があった。特にRubyチュートリアルでは2018/11/18現在、form_for
を利用した方法を紹介しています。が、5.1以降はform_with
推奨とのことなので、書き換えようと思いました。
form_with
APIドキュメントの翻訳版はこちらで読む事ができます。
基準となるモデルがある場合
チュートリアルでは7.2.1で登場してます。Userモデルを作るフォームです。
<%= form_for(@user) do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.label :email %> <%= f.email_field :email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation %> <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %>
form_with
を使用すると下のようにかけます。基本的に変わりませんが、model:
でUserモデルを指定してあげます。チュートリアルの方を見ればわかりますが、コントローラの方で@user = User.new
やってます。
<%=form_with model: @user do |f|%> <%= render 'shared/error_messages', object: @user%> <%=f.label :name%> <%=f.text_field :name,class:'form-control'%> <%=f.label :email%> <%= f.email_field :email,class:'form-control'%> <%=f.label :password%> <%= f.password_field :password,class:'form-control'%> <%=f.label :password_confirmation %> <%=f.password_field :password_confirmation,class:'form-control'%> <%=f.submit yield(:button_text),class:'btn btn-primary'%> <%end%>
基準となるモデルがない場合
一方チュートリアル8.1.2では、上のような誰のフォームであるかをモデルで表わせず、session
という言葉でまとめています。
<%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %>
これは下のようにして書き換えられます。
<%=form_with scope: :session, url:login_path do |f|%> <%=f.label :email%> <%=f.email_field :email, class:'form-control', id:'email'%> <%=f.label :password%> <%=f.password_field :password, class: 'form-control',id: 'password'%> <%=f.submit "Log in", class: "btn btn-primary"%> <%end%>
capybaraでのテスト
form_for
では、生成されるinputタグにid属性が付与されていました。一方form_with
ではid属性はオプションでつくようになっています。上の書き換えの例では、id属性にemail
とpassword
を指定しています。これはcapybaraでfill_in
メソッドを使用してテストするためです。(他にもテスト方法はありますが、、)
#form_for <input class="form-control" id="session_email" name="session[email]" type="email" /> #form_with(id指定なし) <input class="form-control" type="email" name="session[email]"> #form_with(id指定あり) <input class="form-control" id="email" type="email" name="session[email]">
fill_in
メソッドはidを参照するようで、id属性がないとエラーが起きます。変更前は以下のようにテストコードを書いていました。
#capybaraを使用しフォームに値を入れる fill_in "Email", with: "入力する値"
実はこれidがemail
だと反応しません。_email
かEmail
が含まれていれば反応します。 なので、どっちも小文字でemail
を指定するのが一番まるくおさまる気がします。もしくはid属性にEmail
を指定してやればテストコードはそのままで大丈夫かもしれません。
ruby チュートリアル9章 メモ
SecureRandom.urlsafe_base64
Ruby標準ライブラリのメソッド。長さ22文字の文字列を生成してくれる。base64文字列なので64の22乘通りあるため、衝突の可能性も限りなく低い。
アクセサ
インスタンス変数のゲッターとセッターのこと。rubyのクラスのインスタンス変数にはアクセサが無いとアクセスすることができない。attr_accessor
を使用することで自分で書くことなく利用できるようになる。
model(class)内でのselfの意味
チュートリアルでは、変数にself
をつける事でそのインスタンス内で共有が可能な変数を生成している。self
がない場合、ローカル変数になるとの記述があったので、おそらく定義したメソッドのスコープ内でのみ使用可能なものになってしまうはず。
ちなみにメソッドはクラスメソッド内でself
を使用するとクラスを指し、インスタンスメソッド内でselfはそのインスタンスになる。クラスメソッドはmodelクラスのレコードの更新や検索をする用に実装する。
class Article < ActiveRecord::Base def hoge #インスタンスメソッド end def self.hoge #クラスメソッド end def pdf #インスタンスメソッド self.hoge #インスタンスメソッドのhogeが呼ばれる hoge #インスタンスメソッドのhogeが呼ばれる self.class.hoge #こうするとクラスメソッドのhogeを呼べる end def self.pdf #クラスメソッド self.hoge #クラスメソッドのhogeが呼ばれる hoge #クラスメソッドのhogeが呼ばれる end end # https://qiita.com/suzuki_koya/items/1553c405beeb73f83bbc
cookiesメソッド
1つのvalueとオプションのexpires(有効期限)から成っている。下のようにpermanent
を使用すると有効期限が20年に設定される(よく20年設定が使われていた為)
cookies.permanent[:remember_token] = remember_token
それに伴いユーザIDの保存方法も変更する必要がある。signedを使用すると署名付きcookieを使うこと宣言し、permanentではcookieと同じ方法で永続化している。
#変更前 cookies[:user_id] = user.id #変更後 cookies.permanent.signed[:user_id] = user.id
ユーザIDを取得する際はcookies.signed[:user_id]
とする。
remember_me機能
remember_me
機能をチェックの有無でクッキーがブラウザに保存されるかが決まる。これだけでこの機能が実現できているのは、セッションにはチェックに関わらず、変数にユーザ情報を保持させているから。一周回って仕組みが把握しにくくなってきた。
ログアウト時のcookieの削除
ログアウト処理にてcookie
の値が正常に削除されたかをテストするには下のようにする。response
の中身のcookies
を覗かないと値が削除されていないので注意。
expect(response.cookies['user_id'].nil?).to eq false
インスタンス変数のテスト
インスタンス変数へはassigns
を利用することでアクセスが可能になる。チュートリアルではこれを用いることで今までテストされていないかったインスタンス変数をテストした。
#@user内のremember_tokenへアクセスしている expect(response.cookies['remember_token']).to eq assigns(:user).remember_token
ruby チュートリアル8章 メモ
セッションとクッキー
混同しがち(自分だけかも、)HTTPはステートレスなプロトコルなので、セッションのような物を実装するには、状態を保存するものが別に必要なる。それの一つがクッキー。クッキーはユーザのブラウザ内に保存される小さなテキスト。一方セッションはそもそも何かを継続的に行なっているものを指すらしく、接続し続けることもその中の一つと言う解釈でしょうか。
ログインページ
チュートリアルではこれから生成するセッションのコントローラがそのままログインページ系を司どるように作るそうです。言葉にするとわかりにくいので下にそれっぽいルーティングを示します。loginと言うコントローラを作るのではなく、セッションを開始終了のためのページがログイン/ログアウトってイメージですね。なお、セッションコントローラの処理はこれだけでよいので、generate
で無駄なアクションを作ったり、resources
で無駄にルーティングを作るのは避けます。
#config/route.rb get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy'
renderメソッドとアクション
下のようにflashに値を格納した後にrenderを呼ぶと、flashは予期しない挙動をとる。と言うのも、render
でnew
を呼ぶとそれはアクションとは認められないため必然的にflashの生存時間が1アクション分伸びてしまう。
#app/controllers/sessions_controller.rb def create if ~~ # ユーザーログイン後にユーザー情報のページにリダイレクトする else flash[:danger] = 'Invalid email/password combination' render 'new' end end
flash.now
メソッドを利用することでこれを回避することができる。
flash.now[:danger] = 'Invalid email/password combination'
findメソッドとfind_byメソッド
User.find(session[:user_id])
でユーザ検索を行った場合、ユーザIDが存在していない状況(まだログインしていない、session[:user_id]の値がnilになる状態)だと、例外が発生する。一方User.find_by(id: session[:user_id])
は、例外を発生せずnilを返す。
fixtureとFactorybot
チュートリアルではMinitest利用なのでfixtureを利用しているが、RSpecを使用していならFactorybotでカバーできる。個人的には下のようにしてチュートリアルのテストコードを実装しています。
#spec/controllers/user_controller_spec.rb let(:user){build(:user)} let(:post_create){ post( :create, params: { user: { name: user.name, email: user.email, password: user.password, password_confirmation: user.password_confirmation } })} it 'ユーザが新規に生成される' do expect{post_create}.to change{User.count}.by(1) end
t-pot 観察 #6 明け方からアクセス増加
こんにちは。目立つログが残っていたので、メモしておきます。まず、今月のここまでのハニポ全体の記録をみてみます。
今の所今月一のアクセス数です。では中身を見てみます。上のグラフと形が似てしまっていますが、今日(14時まで)の記録です。
Cowrieが検知してますが、どうやら一度にあのアクセス量を受けていたわけでは無いようです。では国別とポート別に分けてみます。
詳細
中国からは211.143.198[.]237から16731程度のアクセス、アメリカからは1162.252.106[.]245から8300程度のアクセスで、どちらもsshへ向いてましたが時間帯は被っておらず。ロシアからはhttpへのアクセスですがこれもピークは他のアクセスとは被ってはおらず128.0.31[.]26から2000程度のアクセスでした。現地時間的にはアメリカとロシアからは夕方頃、中国は明け方頃ですね 。
3つの国からのアクセスは特に珍しくもなかったのですが、時間帯が近いのは珍しいと思います。(少なくとも私の環境だと) ここ最近だとMiraiが怪しいのかと思いますが、httpは確かmiraiの守備範囲ではなかった気がするので(IoTでは無い)、仮にmiraiのbotnetが動き出していたのなら、ロシアのは別攻撃ですね。そうすると中国とアメリカからのピーク時間が微妙に近いのもなんとなく腑に落ちるかもしれない?
ruby チュートリアル7章 メモ
サイトにデバッグ情報表示
下のコードをレイアウトのコードに挿入することで、描画されるページの状態を把握するのに役立つ情報をサイトに表示することができる。if~
文は開発環境でのみデバッグ情報を表示するよう指定している。
#app/views/layouts/application.html.erb <!DOCTYPE html> <html> . . . <body> <%= render 'layouts/header' %> <div class="container"> <%= yield %> <%= render 'layouts/footer' %> <%= debug(params) if Rails.env.development? %> </div> </body> </html>
Sassのミックスイン
ミックスインを使用することでcssルールのグループをパッケージ化して簡単に複数要素に対して使用することができる。例えば、要素のサイズや見た目をいろんな場所で統一したい場合、あらかじめレイアウトをパッケージ化しておけば、そのパッケージをインポートするだけで、レイアウトを適用することできます。
#app/assets/stylesheets/custom.scss @import "bootstrap-sprockets"; @import "bootstrap"; /* mixins, variables, etc. */ $gray-medium-light: #eaeaea; @mixin box_sizing { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } . . . /* miscellaneous */ .debug_dump { clear: both; float: left; width: 100%; margin-top: 45px; @include box_sizing; }
RESTアーキテクチャの適用
データの作成、表示、更新、削除をリソースとして扱うこと。これらに対応する4つの操作(GET,POST,PATCH,DELETE)を各アクションに割り当てる必要がある。RailsのREST機能が有効だと、GETリクエストは自動的にshowアクションとして扱われる。この場合、リソースへの参照はリソース名とユニークIDをURIとして使用することになり、id=1のユーザの参照はusers/1
となる。これをルーティングに設定するには下のようにする。
#config/routes.rb Rails.application.routes.draw do root 'static_pages#home' . . . get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' resources :users end
UsersリソースをRESTfulなアクションにした場合以下のようになる。
debuggerメソッド
byebug gem
のdebuggerメソッド。下のようにコード中に挟むとサーバを立ち上げたコンソール画面から、その場所のデバッグを行うことができる。
#app/controllers/users_controller.rb class UsersController < ApplicationController def show @user = User.find(params[:id]) debugger end def new end end
Gravatar
プロフィール写真をアップロードして、指定したメールアドレスと関連付けることができ、画像パスを生成するだけでその画像を引っ張ってこれる。チュートリアルでは以下のようなヘルパーを作成して画像を引っ張ってきていたが、このチュートリアルに最適かは疑問。後々使うのであればまた別ですが。
# 引数で与えられたユーザーのGravatar画像を返す def gravatar_for(user) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end
オプション引数
メソッドを定義した際に予めデフォルト値を与えてやれば、引数に値を指定しなかった場合自動でデフォルトの値が使用される。もちろん値を渡した場合はその値が優先される。これをオプション引数といい下のように定義する。
def gravatar_for(user, options = { size: 80 }) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) size = options[:size] gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end
またRuby2.0から導入されたキーワード引数を使用することで同じものをより短く実装が可能。
def gravatar_for(user, size: 80) gravatar_id = Digest::MD5::hexdigest(user.email.downcase) gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}" image_tag(gravatar_url, alt: user.name, class: "gravatar") end
これのメソッドの呼び出し方は下のようにする。
<%gravatar_for @user%> #デフォルトの80が適用される <%gravatar_for @user, size:200%> #引数で渡した200がsizeに適用される。
form_forメソッド
form_forメソッドはActiveRecordのオブジェクトを取り込んで、そのオブジェクトの属性を使用してフォームを構築してくれる。
<%= form_for(@user) do |f| %> . . <% end %>
f
オブジェクトはHTMLフォームに対応するメソッドが呼び出されると、@userの属性を設定できる特別なHTMLを返す。labelの:name
シンボルは@userの属性になくてもエラーは出ない。シンボル名がそのままラベルに変換される。text_fieldの方はuserの属性に含まれるものをシンボルとして渡さないとエラーになる。
<%= f.label :name %> <%= f.text_field :name %>
上を実行すると下のように展開される。
<label for="user_name">Name</label> <input id="user_name" name="user[name]" type="text" />
text_fieldを使用すると一般的な入力フォームが、password_fieldを使用すると文字隠しが適用される入力フォームが生成されるなど、柔軟に対応できていることもわかります。
form自身のhtmlは以下のように生成される。
<form action="/users" class="new_user" id="new_user" method="post">
form_forは生成時に読み込んだ@userがUserクラスであり新しいUserであることを自動で認識し、actionとmethodを生成する。RESTfulな設計なのでpostはcreateに対応しており、Userクラスのcreateアクションが実行されるといった流れ。classとidはformヘルパーに対して特に直接的な結びつきは持たない。
-----------追記(2019/3/4)---------
チュートリアルでは、エラーの表示部分をパーシャルとして抽出しているのですが、チュートリアルのままだと@user
変数にしか使えません。下のサイトを参考にどのフォームでもこのパーシャルを使い回せる様にしましょう。
qiita.com
= form_with(model: resource, as: resource_name, url: password_path(resource_name),local:true, html: { method: :post }) do |f| = render "shared/error_messages", model: f.object # 追加:modelで参照できる様にする .field = f.label :email br/ = f.email_field :email, autofocus: true, id: "email" , autocomplete: "email", class: "form-control" .actions = f.submit I18n.t("devise.passwords.new.send_me_reset_password_instructions"), class: "btn btn-primary" = render "devise/shared/links"
ごめんなさい、Slim
での記法になってます。
# _error_messages.html.slim - if model.errors.any? # modelでオブジェクトの参照が可能 .error-explanation.alert.alert-danger strong = I18n.t("errors.messages.not_saved", count: model.errors.count, resource: model.class.model_name.human.downcase) ul - model.errors.full_messages.each do |message| = message
-----------追記ここまで-----------
Strong Parameters
user情報入力後、その値を全てそのままUser生成に使用してしまったら、意図しないパラメータも更新されてしまう可能性がある。そのため、予め設定したものだけのみ、入力情報から取り出せるよにするべきである。
@user = User.new(params[:user]) #:userに格納された全ての情報を使用しようとする @user = User.new(params.require(:user).permit(:name, :email, :password, :password_confirmation)) #許可した値だけ取り出される。
例えばcreateアクション時に簡単に呼び出せるようなメソッドをprivateに実装しておくことが考えられる。
def create @user = User.new(user_params) if @user.save # 保存の成功をここで扱う。 else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end
app/views/sharedディレクトリ
複数のビューで使用されるパーシャルはこの下に保存される。(習慣なので自動でやるわけではない)
pluralize
英語テキスト専用のヘルパー。一つ目の引数の値(数字)によって二つ目の文字を複数形に変更してくれる。
redirect_to
redirect_to @user
はredirect_to user_url(@user)
と等価。これは以下の挙動と同じだが、あくまで挙動が同じだけ。route.rbでresourcesを指定していないものに対しては使用できない(user_urlがresourcesを使用した場合のみ利用可能になるから)
redirect_to "/users/#{@user.id}" # 条件付き(後述) or redirect_to user_url(id: @user.to_param) or redirect_to user_url(id: @user.id)
flash
ページが切り替わった時にユーザに向けて表示するメッセージを生成管理してくれるメソッド。コントローラのアクション定義の際にflashにシンボルと紐づけてメッセージを格納すると、次のアクションするまでその値を保持してくれる。(createアクション内で格納したメッセージは次のアクションで飛んだページでは残っているが、もう一度アクションをすると消える)
【Rails入門】flashの使い方まとめ | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
なお、次のアクションにも残さなかったり、逆に永続的に保持させることも可能。
content_tag
railsのヘルパー。content_tag(タグの名前)
といった形で使用し、タグを動的に生成してくれる。チュートリアルではflash用のhtmlを見やすいように書き換えるために使用した。
#before <% flash.each do |message_type, message| %> <div class="alert alert-<%= message_type %>"><%= message %></div> <% end %> #after <% flash.each do |message_type, message| %> <%= content_tag(:div, message, class: "alert alert-#{message_type}") %> <% end %>
今回のintegrationテストに関して
今回のサインアップのテストはチュートリアル上ではIntegrationテスト(featureテスト)の位置付けで行われていた。しかし、その内容はコントローラのテストとした方がよい可能性がある。
と言うのも、featureテストはcontrollerを挟んだviewをテストするイメージであり、今回のテストは完全にcontrollerのメソッド内で完結している処理だからである。
では、viewのテストとは?一般にはviewのテストは書かないという風潮があるかもしれない。。何かを表示するにはほぼ必然的にcontrollerを経由し、それはつまりfeatureテストになる。(getしたページのタイトルが予期したものか等) またそもそもページに何のコンテンツが表示されているかのテストを始めると、保守コストが高すぎてやってられるか〜!となる話もあったりなかったり。
(念のためですが、書いている方もしっかりいらっしゃるので決して必要ないと言うことではありませんよ=> RSpec使ってERBに対するspecをどう書くか? - Qiita)
ちなみに、viewのヘルパーのテストは行う可能性が十分にあるので注意。