大まかな流れの自己整理が目的のため、不足・誤り等あれば追記&訂正していきますのでご指摘頂けますと幸いです
なお、筆者はYassLabさんの動画版で学んでいるため、本記事は「チュートリアル sample_app」+「他補足」個人的に「電子ページ以上に分かりやすい!」と感じた解説部分+参考記事を整理してみようと試みた劣化の内容寄りになってます。
7.1 ユーザーを表示する
params
Railsで送られてきた値を受け取るためのメソッド(その後Railsが保存を行う)。中身はハッシュ。
主に、以下の2つ
・投稿フォームなどPOSTで送信されたデータ
・検索フォームなどGETで送信されURLにクエリとして入るデータ
ex.params[:id] → ユーザなどのid
<参考>
【Rails】paramsについて徹底解説!
【Rails入門】params使い方まとめ
debug(メソッド)
paramsの中身(表示画面)を出力してくれる。putsのような扱い。
本番環境に展開したアプリケーションではデバッグ情報を表示したくないので、後置if文でデベロッパー確認を追加する。
(後置if文は1行で済むときによく使用される)
app/views/layouts/application.html.erb 内に下記追加
<%= debug(params) if Rails.env.development? %>
補足
Railsにはテスト環境 (test)、開発環境 (development)、そして本番環境 (production) の3つの環境がデフォルトで装備されている。Rails consoleのデフォルトの環境はdevelopment。
app/assets/stylesheets/custom.scssに
デバッグ表示を整形するための追加と、Sassのミックスインを追加する(ここでは省略)
/users/1 のURLを有効にするため、routes(るーつ、らうつ)ファイルsignup下に追加
resources :users
確認(今回注目するのはnewアクションとshowアクション)
$ rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
static_pages_home GET /static_pages/home(.:format) static_pages#home
help GET /help(.:format) static_pages#help
about GET /about(.:format) static_pages#about
contact GET /contact(.:format) static_pages#contact
sighup GET /sighup(.:format) users#new
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
putリクエスト → 全部(旧残り)
patchリクエスト → 欲しい情報の一部(現在主流)
Usersコントローラにshowアクションを追加する(showアクションはGET /users/:id と連動)。挿入場所(順番)は可読性を考慮し出来ればRESTfulなルート順(表)が望ましい。
@user
はインスタンス変数とされ、showアクション外(例えばviewであるshowテンプレート: => app/views/users/show.html.erb)からも呼び出すことが出来るため、「paramsで持ってきたユーザid(リクエスト値)を@user
に格納する」ような流れ。
def show
@user = User.find(params[:id])
end
HTTPリクエスト | URL | アクション | 名前付きルート | 用途 |
---|---|---|---|---|
GET | /users | index | users_path | すべてのユーザーを一覧するページ |
GET | /users/1 | show | user_path(user) | 特定のユーザーを表示するページ |
GET | /users/new | new | new_user_path | ユーザーを新規作成するページ (ユーザー登録) |
POST | /users | create | users_path | ユーザーを作成するアクション |
GET | /users/1/edit | edit | edit_user_path(user) | id=1のユーザーを編集するページ |
PATCH | /users/1 | update | user_path(user) | ユーザーを更新するアクション |
DELETE | /users/1 | destroy | user_path(user) | ユーザーを削除するアクション |
サンプルアプリケーションを既にHeroku上にデプロイしている場合は、heroku run rails consoleというコマンドを打つことで、本番環境を確認することができます。
$ heroku run rails console
>> Rails.env
=> "production"
>> Rails.env.production?
=> true
デバッガー(debugger)
追記して稼働させるとゆっくり読み込み中になる
def show
@user = User.find(params[:id])
# => app/views/users/show.html.erb
debugger
end
rails サーバを立ち上げると、止まってる場所がわかる
def show
4: @user = User.find(params[:id])
5: # => app/views/users/show.html.erb
6: debugger
=> 7: end
8:
9: def new
10: end
11: end
(byebug)
コマンドはlist,nextなど。exit、ctrl+cで終了。
使い終わったらコメントアウト。
show.html.erbにgravatar_forヘルパーメソッドを使い、WordPress系アバター画像のGravatarの画像を利用できるようにする。
<% provide(:title, @user.name) %>
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
ヘルパーメソッドとしてusers_helper.rbに追加する。
最後の行のimage_tag(url、alt&class属性付与)がgravatar_forに返される。
なお、gravatar_forメソッドでのemailは(has_secure_passwordのように)Digest::MD5によってハッシュ化されている。
module UsersHelper
# 引数で与えられたユーザーの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
end
この時点でデフォルト画像のまま表示されるが、show.html.erbにasideタグ(サイドバーなど補足として付け加えたいときに便利なもの)を付け加える。
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
</div>
saasに追記してフォーマット等調整。
/* sidebar */
aside {
section.user_info {
margin-top: 20px;
}
section {
padding: 10px 0;
margin-top: 20px;
&:first-child {
border: 0;
padding-top: 0;
}
span {
display: block;
margin-bottom: 3px;
line-height: 1;
}
h1 {
font-size: 1.4em;
text-align: left;
letter-spacing: -1px;
margin-bottom: 3px;
margin-top: 0px;
}
}
}
.gravatar {
float: left;
margin-right: 10px;
}
.gravatar_edit {
margin-top: 15px;
}
7.2 ユーザー登録フォーム
コントローラのnewアクションに空のオブジェクトを作成して@user変数を追加する
def new
@user = User.new
end
view画面のnewに追記。
form_forはブロック付き引数で、ブロック内部では例として「email」というキーの中にユーザのバリューが入るイメージ。
「f.label」はラベルのなので削っても問題ないが、あるとユーザ視点で何を入力していいか分かりやすい。(変更イメージの参考:[rails]ActiveModelを使ったフォームのラベル名を変更する)
「f.submit」は"Create my account"というボタンからこれらのデータをnewアクション→createアクションへ送る(次のアクションへ)。
<h1>Users#new</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= 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 %>
</div>
</div>
formのscss追記
/* forms */
input, textarea, select, .uneditable-input {
border: 1px solid #bbb;
width: 100%;
margin-bottom: 15px;
@include box_sizing;
}
input {
height: auto !important;
}
そのまま作るとエラーが起きるが、「newアクション移行→createアクションがないよ」という表示なので正常。
createアクション追加&renderでビュー画面newに戻り、登録情報確認(試験的にfoobar入れてます)
def create
@user = User.new
render 'new'
end
7.3 ユーザー登録失敗
ユーザー登録の失敗に対応できるcreateアクション
オプション引数について
ハッシュのハッシュなので、userの中に[:name],[:email],[:password]があるので:userで省略。
def create
# オプション引数はキーワードにシンボルが入ってバリューに値が入ってる集合体
# → User.new(name: ..., email:, ...)
# @user.name = params[:user][:name]
# @user.user = params[:user][:email]
# @user.password = params[:user][:password]
# 1行で完結
@user = User.new(params[:user])
if @user.save #=> Validation
# Sucess
else
#Failure
render 'new'
end
end
エラー登場。createの直後のparamsはユーザが送る情報なのでいろいろな情報を送る(いじる)ことができるので、今後adminなどを入れていく過程で悪意あるユーザからDB書き換えたれるなどのマスアサインメント脆弱性を回避するRails4.0移行実装の機能。paramsハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしたいので、requireなどを追加したuser_paramsメソッドをコントローラに追加してメソッドで対応する。
def create
@user = User.new(user_params)
if @user.save #=> Validation
# Sucess
else
#Failure
render 'new'
end
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
Rails consoleからメソッド「.errors.full_messages」でエラー詳細表示
空のユーザ作成後save失敗から.full_messagesメソッドでエラーの詳細表示ができる。
(今回はいろいろ空だったことが原因)
2.6.3 :002 > @user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil>
2.6.3 :003 > @user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT ? [["LIMIT", 1]]
=> false
2.6.3 :004 > @user.save
(0.1ms) begin transaction
User Exists (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT ? [["LIMIT", 1]]
(0.1ms) rollback transaction
=> false
2.6.3 :005 > @user.errors.full_messages
=> ["Name can't be blank", "Email can't be blank", "Email is invalid", "Password can't be blank", "Password can't be blank", "Password is too short (minimum is 6 characters)"]
ユーザ登録時にエラーメッセージが出るよう追記する。sharedディレクトリもviewディレクトリ下に作る。
<h1>Users#new</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= 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, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
パーシャル(Viewでの似たようなコードを一つでまとめてるもの)を作るがアンダーバーなので注意
$ cd app/views/
$ mkdir shared
$ c9 open _error_messages.html.erb
「.errors.eny?」 → save,validなど何か実行されてエラーがなければfalse(何も表示しない)、「.count」あるので1個以上あればtrueを返す。pluralizeは"error"を個数に応じて単数・複数形で判断してくれる。
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
「Password can't be blank」が2つ出る理由は、「has_secure_password」と「validates presence」で引っかかっているためのバグ。
応急処置として、has_secure_password内に下記を追記するなどがある。
allow_nil: true
エラー表示のSCSS追加
#error_explanation {
color: red;
ul {
color: red;
margin: 0 0 30px 0;
}
}
.field_with_errors {
@extend .has-error;
.form-control {
color: $state-danger-text;
}
}
<form class="new_user" id="new_user" action="/users" accept-charset="UTF-8" method="post">
とあり、
アクションが「/users」、メソッドが「post」から、「postリクエストを/usersに送りつける」ことが分かる。
ポストリクエストの流れは下記から確認でき、
「POST」リクエストが「/users」に送られると、「users」コントローラの「create」アクションが反応する。
$ rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
static_pages_home GET /static_pages/home(.:format) static_pages#home
help GET /help(.:format) static_pages#help
about GET /about(.:format) static_pages#about
contact GET /contact(.:format) static_pages#contact
signup GET /signup(.:format) users#new
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
「assert_no_difference」を使った、失敗時のテスト
インテグレーションテスト(結合テスト)から
$ rails generate integration_test users_signup
Running via Spring preloader in process 4226
invoke test_unit
create test/integration/users_signup_test.rb
自動生成されたものに追記。「signup_path」にgetリクエストを送りつける。
postリクエストをusers_pathに送り、その時にパラメータ:{ user: { name:,email:,password:,password_confirmation}}(オプション引数)を送りつける。
assert_no_differenceは呼び出す前後で値に違いがないことを主張するテストで、引数に(User.count)が入っていることから、「ユーザ数を覚えた後にデータを投稿してみて、ユーザ数が変わらないかどうかを検証するテスト」になる(公式より)。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" } }
end
assert_template 'users/new'
end
end
この時点でテスト(通過)
$ rails t
補足(エラー 「undefined local variable or method `signup_path'」)
筆者の場合、この時点で下記のようなエラーが出ました。
$ rails t
Running via Spring preloader in process 4176
Run options: --seed 61612
# Running:
.E..........
Finished in 0.618448s, 19.4034 runs/s, 30.7221 assertions/s.
1) Error:
UsersSignupTest#test_invalid_signup_information:
NameError: undefined local variable or method `signup_path' for #<UsersSignupTest:0x00000000065b4838>
test/integration/users_signup_test.rb:6:in `block in <class:UsersSignupTest>'
12 runs, 19 assertions, 0 failures, 1 errors, 0 skips
「自分でアカウント追加して遊んだから講義とユーザ数ずれちゃったせいかな?」と考えたりしてましたが(このテストでは前後数のみで関係ない)
ルート(routes.rb)、ビュー(home.html.erb)でsignup_pathがsighup_pathにタイプミスしてました
7.4 ユーザー登録成功
createアクションにリダイレクトを追加する(省略過程あり)。
def create
@user = User.new(user_params)
# Sucess
# redirect_to user_path(@user.id)
# user_pathの引数デフォルトがidなので「.id」省略可、
# redirect_to user_path(@user)
# さらにredirect_toのデフォルト挙動としてユーザオブジェクトを渡すとuser_pathになるので
redirect_to @user
#GETリクエスト(が右にいく) => "/users/#{@user.id}" => showアクションが動く
else
#Failure
render 'new'
end
end
flash
登録完了後に表示されるページにメッセージを表示する (この場合は新規ユーザーへのウェルカムメッセージ)。
createアクション(if文直下)と、ビュー(yield直前)に追加
flash[:success] = "Welcome to the Sample App!"
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
↑(ビュー側のerb埋め込みruby)の補足
ハッシュはキー+バリューの仕組みになっていて、
eachメソッドで呼び出すと、message_type(キー:success)とmessage(バリュー:"Welcome to the Sample App!")の関係になっている。
また、classの中にmessageを埋め込んでいる。
登録動作を確認してみると、5番目のユーザで作成できてる(成功!)。
flashは一度だけなのでリロードすればメッセージ(緑色)は消える。
成功時のテスト
成功時、ユーザのカウントが1増えてればokという内容。
基本的にはshowテンプレートが表示されていれば🙆♂️
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
end
URLを確認すると「https://〇〇.herokuapp.com/users/1
」となっており、
本番環境(heroku)では1人目のユーザであることが分かる。
7.5 プロのデプロイ
本番環境でのSSL
herokuのサブドメイン「https://〇〇.herokuapp.com(2番目)
/users/1」を使っている内はサービス上SSL化(🔒Secure)されている。(Googleなどではhttpより検索順位が上位になったりするので重要であり、将来的に独自ドメインを使っていく際は自分で証明書の発行が必要になる。)
https(SSL)通信を本番環境で強制するやり方として、production(本番環境).rbに下記を追記(コメントアウト解除してtrueに)する。
これにより、もしhttpでアクセスしても問答無用でhttpsに切り替わる。
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
本番環境に適したWebサーバーを構築する
heroku推奨の設定ファイルをpumaに書き換える。
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count
preload_app!
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
on_worker_boot do
# Worker specific setup for Rails 4.1+
# See: https://devcenter.heroku.com/articles/
# deploying-rails-applications-with-the-puma-web-server#on-worker-boot
ActiveRecord::Base.establish_connection
end
Pumaがheroku上で使うようにプロックファイル(./Procfile)(はデフォルトでないので)作成し、定義する。
/sample_app $ c9 open Procfile
web: bundle exec puma -C config/puma.rb
一応テスト走らせた後、
再度herokuへデプロイ(コミットメッセージが分かるように)して終了。
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
感想ほか
講義が非常に分かりやすく全く挫折することなく終えました(講師:安川さん、ありがとうございました!!)。
教材模写(受け身)が中心だったので、忘れた部分含め2周目自力で進めて公式ドキュメント漁って読み込むなどもっと頭を悩ませる必要はあるかなと。
最後までお読み頂きありがとうございました!!