Day8です!
今日もがんばっていきましょー!
読んでいただいている方、いつもありがとうございます!
初心者なもので、たまに間違った内容もあるかと思います。
その際にはコメントやTwitterでご指摘いただけると幸いです!
[Day8]第七章7.3.3〜7.6作業ログ
エラーメッセージはmodelsで自動生成
$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?> password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
.errors.full_messages
メソッドでエラーメッセージを配列として表せる。
これはパーシャルという機能を使って、別のHTMLファイルから呼び出すようにする。
Rails全般の慣習として、複数のビューで使われるパーシャルは”shared”というディレクトリに置かれる。
ない場合はmkdir app/views/shared
で作成。
パーシャルについてはDay5でも出てきたので、そちらも参考に。
参考:[Day5 Rails Tutorial]第五章完|Webアプリっぽくなってきた!テスト楽しい[5.1〜5.5]
登録フォームの完成
今のままだと、正しい情報を送信しても、createのviewがないとエラーがでます。
viewを作成してもいいんだけど、createアクションにはviewを用意しないのが普通。
代わりにユーザーページにリダイレクトするようにします。
class UsersController < ApplicationController
...
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render 'new'
end
end
...
end
redirect_to @user
がわかりにくかったので調べた。
これは次の挙動と同じらしい。
redirect_to "/users/#{@user.id}"
※ただし、routeでresource使用時のみに限るredirect_to user_url(id: @user.id)
redirect_to user_url(@user)
これは上を簡単にした表現かな?
一番上がわかりやすい。
フラッシュメッセージの表示
flash変数に代入した値は、リダイレクトした直後のページで表示される。
またページを更新すると消える。
具体的には、こんなコードを挿入。
class UsersController < ApplicationController
...
def create
@user = User.new(user_params)
if @user.save
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
...
end
end
...
end
<!DOCTYPE html>
<html>
...
<body>
<%= render 'layouts/header' %>
<div class="container">
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
<%= yield %>
<%= render 'layouts/footer' %>
<%= debug(params) if Rails.env.development? %>
</div>
...
</body>
</html>
プロのデプロイ|セキュリティを視点に
まずはこれまでの変更をマージしておきます。
セキュリティを視点としたプロのデプロイを実践。
具体的にはSSL化設定。
HerokuではデフォルトでSSLが使われますが、そのままではSSLの利用をブラウザに強制しないため、ユーザーが(httpsでない)通常のhttpでアクセスしてしまうと、サイトとユーザーの間のやりとりが安全ではなくなってしまいます
でも、SSLを強制するのは簡単で、ここをコメントアウトするだけでOKです。
Rails.application.configure do
...
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true
...
end
次に通常は遠隔サーバーのSSLのセットアップが必要。
ですが、Herokuのサブドメインを使用している場合は、Heroku上でサンプルアプリケーションを動かし、HerokuのSSL証明書に便乗できるそう。
独自ドメインでやるときは、ドメイン毎にSSL証明書を購入し、セットアップする必要あり。
(そういえば、このブログでも設定したなぁ)
本番環境用のサーバー設定
Herokuのデフォルトでは、Rubyだけで実装されたWEBrickというWebサーバを使っています。WEBrickは簡単にセットアップできたり動せることが特長ですが、著しいトラフィックを扱うことには適していません。つまり、WEBrickは本番環境として適切なWebサーバではありません。よって、今回はWEBrickをPumaというWebサーバに置き換えてみます。Pumaは多数のリクエストを捌くことに適したWebサーバです。
だそうです!
つまりはサーバーをPumaっていうWebサーバーに変えましょうとのこと。
アプリのルートディレクトリにProcfileファイルを作成して、次のコードを書くだけでOK。
web: bundle exec puma -C config/puma.rb
最後はプッシュして終了
rails test
(一応確認のテスト)
git add -A
git commit -m "Use SSL and the Puma webserver in production"
git push
git puch heroku
タケシなりの演習の回答(第七章)
リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします
細かさはおまかせだったので、HTMLがちゃんと入ってるかを調べるテストを書きました。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
...
assert_template 'users/new'
assert_select 'body div.col-md-6 div#error_explanation'
assert_select 'body div.col-md-6 ul'
end
end
HTMLの存在をテストするassert_select
で上のコードのように書いてあげると、入れ子のように要素が検証できる。
今回の場合だと、bodyの中に”col-md-6″クラスを持ったdiv要素があって、さらにその中に”error_explanation”というidをもったdiv要素があるか、検証している。
今回の演習では、こちらのページがとても参考になりました。
リスト 7.27のフォームを以前の状態 (リスト 7.20) に戻してみて、テストがやはり greenになっていることを確認してください。これは問題です! なぜなら、現在postが送信されているURLは正しくないのですから。assert_selectを使ったテストをリスト 7.25に追加し、このバグを検知できるようにしてみましょう
問題は送信先のURLが違うこと。公式ドキュメントを参考に、送信先URLを調べてみた。
(1)オプション無しの場合(リスト7.20)
<%= form_for(@user) do |f| %>
<% end %>
# <form action="/users" class="new_user" id="new_user" method="post">
# </form>
(2)urlオプションありの場合(リスト7.27)
<%= form_for(@user, url: signup_path) do |f| %>
<% end %>
# <form action="/signup" class="new_user" id="new_user" method="post">
# </form>
この部分に着目して、action="/signup"
があるかどうか検証するテストコードをかけばOK。
こんな感じ。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
...
assert_template 'users/new'
assert_select 'body div.col-md-6 div#error_explanation'
assert_select 'body div.col-md-6 ul'
assert_select 'form[action="/signup"]'
end
end
7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
...
test "valid signup information" do
...
assert_not flash.empty?
end
end
[Day8]第八章8.1〜8.4作業ログ
この章からはじめのトピックブランチの作成、最後のマージは書かないことにします。
HTTPはステートレスなプロトコル
HTTPはステートレスなプロトコルで、基本的にリクエストはその場限り。
リクエストが終わると、またゼロから新しいリクエストができるイメージ。
そこで、ログインが必要なWebアプリケーションでは、別途セッションという半永久的な値を設定する。
セッションはHTTPプロトコルと階層が異なる (上の階層にある) ので、HTTPの特性とは別に (若干影響は受けるものの) 接続を確保できます。
このセッションという値で、ログインなどを管理する。
ログインフォームのコードを追加
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= 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 %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
<form accept-charset="UTF-8" action="/login" method="post">
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
<label for="session_email">Email</label>
<input class="form-control" id="session_email"
name="session[email]" type="text" />
<label for="session_password">Password</label>
<input id="session_password" name="session[password]"
type="password" />
<input class="btn btn-primary" name="commit" type="submit"
value="Log in" />
</form>
フォームから送信されたemailとpasswordがsessionというハッシュに入る感じ。
sessionもparamsのハッシュに入っているので、こんな構造。
params = { session: { password: "foobar", email: "user@example.com" } }
フラッシュメッセージの微妙な違い
flashメッセージはリダイレクトされると消えるけど、renderされると、更新しない限りずっと残ってしまう。
そこでflash.nowを使う。
- flash:renderされると消えない
- flash.now:renderでも消える。ホントにその時だけ表示。
sessionメソッドでユーザーIDを保持
session[:user_id] = user.id
このコードでユーザーのidをsession[:user_id]
に入れられる。
これもcookieの1つだけど、ブラウザを閉じると消える。
対象的にcookiesメソッドを使えば、ブラウザを閉じても保持できる値を使える。
現在のユーザー情報を所得するメソッドの設定
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
end
@current_user ||= User.find_by(id: session[:user_id])
を詳しくみていく。
これは足し算引き算で出てくる、これらが同じなのと一緒。
- x = x + 1
- x += 1
つまり、@current_user = @current_user || User.find_by(id: session[:user_id])
と同じ。
はじめは分からなかったけど、下の説明でスッキリ。
Rubyでは、||演算子をいくつも連続して式の中で使う場合、項を左から順に評価し、最初にtrueになった時点で処理を終えるように設計されています。なお、このように||式を左から右に評価し、演算子の左の値が最初にtrueになった時点で処理を終了するという評価法を短絡評価 (short-circuit evaluation) と呼びます。
つまり@current_userがnilだったら右の値が代入され、nilでなければ左の値を保持するという式。
ちなみに、&&演算子にも似たような設計で、
論理積の&&演算子も似たような設計になっていますが、項を左から評価して、最初にfalseになった時点で処理を終了する点が異なります。
らしい。
ログイン時とログアウト時でヘッダーメニューを変える
全然難しいことはなくて、@current_userがいるか判定するメソッドを追加して、HTMLファイルにifで分岐を作るだけ。
module SessionsHelper
...
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
end
end
<% if logged_in? %>
<li>略</li>
<% else %>
<li>略</li>
<% end %>
ログインのテスト
ログイン時とログアウト時で表示が違うことをテストしたい。
そのためには当然有効なユーザー情報がいるけど、Railsではこういったテスト用データをfixture (フィクスチャ)で作成できるらしい。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
注意:ヘルパーメソッドはテストから呼び出せない
だからさっき作成したlog_inメソッドと似たメソッドをtest_helper.rb
にもちょっと名前を変えて定義しておく。
ENV['RAILS_ENV'] ||= 'test'
...
class ActiveSupport::TestCase
fixtures :all
# テストユーザーがログイン中の場合にtrueを返す
def is_logged_in?
!session[:user_id].nil?
end
end
このコードを使ってテストしてあげる。
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
...
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
...
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
end
ログアウト機能の実装
ログアウトはsessionの値を削除して、@current_userをnilにしてあげればOK。
class SessionsController < ApplicationController
...
def destroy
log_out
redirect_to root_url
end
end
ログアウトのテスト
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
...
test "login with valid information followed by logout" do
get login_path
post login_path, params: { session: { email: @user.email,
password: 'password' } }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
follow_redirect!
assert_select "a[href=?]", login_path
assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
end
今日は長かった!おつかれさまでした!
メモ(第八章)
wc(wordcount)
(ちょっと寄り道してたらひっかかったので一応載せておく)
あるファイルの中身の文字数を数えてくれる。
実験として、Gemfileの単語数などを読み取ってみます。
environment/sample_app (basic-login) $ wc Gemfile
35 88 880 Gemfile
これは、35行、88単語、880バイトのデータが入っているという結果です。
grepコマンドは文字列を検索できる
grepコマンドは文字列含まれている行を表示してくれるコマンド。
使い方はこれ。
grep [オプション] 検索パターン(文字列) ファイル
コマンド | grep [オプション] 検索パターン
※[ ]は省略可
下はパイプという、コマンドとコマンドを繋げて実行できる方法。
演習で使った内容。
タケシなりの演習の回答(第八章)
ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。
メモ(第八章)にまとめた、パイプと grepを使ってあげる。
~/environment/sample_app (basic-login) $ rails routes | grep -w users
signup GET /signup(.:format) users#new
POST /signup(.:format) users#create
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
同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。
~/environment/sample_app (basic-login) $ rails routes | grep -w sessions
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか?
下のURLを参考に確認できました。
きちんとハッシュ化されていて、長い英数字の文字列でした。
参考:GoogleChromeで特定のサイトのクッキー(Cookie)情報を確認する方法
[Day8]まとめ
- 学習範囲:7.3.3〜8.4
- 学習時間:6時間30分
- 総学習時間:37時間20分
- 反省点:ちょっと今日は長めだったので、集中力が切れそうだった。第八章まで終わりっていうことにこだわらずもっと区切ったらよかったかも。でもDay8で第八章ってなんかいい感じやん?
- 備考:ちょこちょこ微妙な所ある。また明日のあさ復習しよう。
読んでいただいている方、いつもありがとうございます!
初心者なもので、たまに間違った内容もあるかと思います。
その際にはコメントやTwitterでご指摘いただけると幸いです!
コメントを残す