見出し画像

railsチュートリアル挑戦記 第7章 ユーザー登録

railsチュートリアルをやりながらメモしたことをそのまま記述しています。

第7章ユーザー登録

新規ユーザーをどんどん増やしていく

7章から難易度が少しずつ上がっていくとのこと

・7 1 ユーザーを表示する

いつものようにトピックブランチを作成
git checkout -b sign-up

・7 1 1 デバッグとRails環境

debugメソッドとparams変数を使って、各プロフィールページにデバッグ用の情報が表示されるようにする

サイトのレイアウトにデバッグ情報を追加する
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>
----------------------------


if Rails.env.development?
これは「開発環境?」という意味

if Rails.env.test?
if Rails.env.production?
本番環境やテスト環境でデバッグ情報を表示させないべきらしい

railsコンソールをテスト環境で、とかやれるらしい
rails console test

railsサーバーも本番環境で、とか指定できるらしい
rails server --environment production

マイグレーションも本番環境で、とか指定できるらしい
rails db:migrate RAILS_ENV=production

ほんで、デバッグ出力をきれいに整形するために、cssを弄る

デバッグ表示を整形するための追加と、Sassのミックスイン.
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;
}
----------------------------

下のインクルードに上の定義が展開されてくイメージ

Webページ上のログ
----------------------------
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
 controller: static_pages
 action: home
permitted: false
----------------------------

aboutページは
controller: static_pages
action: about
だった

サンドボックス上でu=User.find(1)して
puts u.attributes.to_yaml
ログ
----------------------------
---
id: 1
name: test
email: mhartl@example.com
created_at: !ruby/object:ActiveSupport::TimeWithZone
 utc: &1 2020-01-01 15:55:01.546006000 Z
 zone: &2 !ruby/object:ActiveSupport::TimeZone
   name: Etc/UTC
 time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
 utc: &3 2020-01-01 16:11:07.015681000 Z
 zone: *2
 time: *3
password_digest: "$2a$10$3bYK77wXYp0SyMoA7FfsHOsuPabfPoNF3ivprqMe.Q715LPQuLTdq"
=> nil
----------------------------

見やすい


・7 1 2 Usersリソース

/users/1という感じのURLを有効にするために以下のようにする

Usersリソースをroutesファイルに追加する
config/routes.rb
----------------------------
Rails.application.routes.draw do
 root 'static_pages#home'
 get  '/help',    to: 'static_pages#help'
 get  '/about',   to: 'static_pages#about'
 get  '/contact', to: 'static_pages#contact'
 get  '/signup',  to: 'users#new'
 resources :users
end
----------------------------

resources :usersとすると!なんと以下のようになる!! RESTful URI
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) ユーザーを削除するアクション

便利ィ!

show用のerbを手動で作成する

ユーザー情報を表示するための仮のビュー
app/views/users/show.html.erb
----------------------------
<%= @user.name %>, <%= @user.email %>
----------------------------

そして、@userを作る必要がある

Usersコントローラのshowアクション
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 def show
   @user = User.find(params[:id])
 end
 def new
 end
end
----------------------------

params[:id]は、全て特殊な文字列
urlに含まれてる数字
しかし文字列になる
更に言うとfindに渡すと自動的に整数型に変換される
うまくできてるなー

ログ
----------------------------
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
 controller: users
 action: show
 id: '1'
permitted: false
----------------------------

・7 1 3 debuggerメソッド

byebugでもっと直接的にデバッグできるらしい

debuggerをUsersコントローラに差し込む
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 def show
   @user = User.find(params[:id])
   debugger
 end
 def new
 end
end
----------------------------


以下のようにコンソールでコマンドを呼び出せる

ログ
----------------------------
(byebug) @user.name
"Example User"
(byebug) @user.email
"example@railstutorial.org"
(byebug) params[:id]
"1"
----------------------------
debuggerをUsersコントローラーから取り外す
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 def show
   @user = User.find(params[:id])
 end
 def new
 end
end
----------------------------

演習

ログ
----------------------------
(byebug) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
 controller: users
 action: show
 id: '1'
permitted: false
nil
----------------------------

/users/1にアクセスしてbuybugで
@userにはnilが入ってた

・7 1 4 Gravatar画像とサイドバー

gravatar_forヘルパーメソッドを使ってGravatarの画像を利用できるようにする

ユーザー表示ビューに名前とGravatarを表示する
app/views/users/show.html.erb
----------------------------
<% provide(:title, @user.name) %>
<h1>
 <%= gravatar_for @user %>
 <%= @user.name %>
</h1>
----------------------------
gravatar_forヘルパーメソッドを定義する
app/helpers/users_helper.rb
----------------------------
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
----------------------------

hexdigestは、大文字と小文字で異なるハッシュを返してしまうので注意!

ここまでで、デフォルトのユーザーページが表示されるはず

以下のコマンド
----------------------------
$ rails console
>> user = User.first
>> user.update_attributes(name: "Example User",
?>                        email: "example@railstutorial.org",
?>                        password: "foobar",
?>                        password_confirmation: "foobar")
=> true
----------------------------

これで、railsチュートリアルのロゴが表示されるはず
なんかGravatarに割り当ててるかららしい

おおおおお!ロゴ変わった!!!


ユーザーのshowビューにサイドバーを追加する
app/views/users/show.html.erb
----------------------------
<% 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>
----------------------------
SCSSを使ってサイドバーなどのユーザー表示ページにスタイルを与える
app/assets/stylesheets/custom.scss
----------------------------
.
.
.
/* 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;
}
----------------------------

Gravatar上にアカウント作成してメールアドレスと適当な画像紐づけてやるのはまた今度

gravatar_forヘルパーにオプション引数を追加する
app/helpers/users_helper.rb
----------------------------
module UsersHelper
 # 引数で与えられたユーザーのGravatar画像を返す
 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
end
----------------------------

gravatar_forヘルパーにキーワード引数を追加する
app/helpers/users_helper.rb
----------------------------
module UsersHelper
 # 引数で与えられたユーザーのGravatar画像を返す
 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
end
----------------------------

・7 2 ユーザー登録フォーム

モックアップ見せられた

・7 2 1 form_forを使用する

newアクションに@user変数を追加する
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 def show
   @user = User.find(params[:id])
 end
 def new
   @user = User.new
 end
end
----------------------------

新規ユーザーのためのユーザー登録フォーム
app/views/users/new.html.erb
----------------------------
<% provide(:title, 'Sign up') %>
<h1>Sign up</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>
----------------------------
ユーザー登録フォームのCSS
app/assets/stylesheets/custom.scss
----------------------------
/* forms */
input, textarea, select, .uneditable-input {
 border: 1px solid #bbb;
 width: 100%;
 margin-bottom: 15px;
 @include box_sizing;
}
input {
 height: auto !important;
}
----------------------------


・7 2 2 フォームHTML

なぜform_forを使ったか?
あとなぜfか?
fは特別っぽい。
メアド入力で最適なキーボードが表示されたりするし
パスワードは●●●みたいになるし至れり尽くせり

inputのname属性は超重要
params変数経由で取りに行くため

次に重要なのがformタグ自身 actionとmethodが重要

まあここは座学って感じだった


・7 3 ユーザー登録失敗

入力が失敗したときにエラーを表示するようにする

・7 3 1 正しいフォーム

ユーザー登録の失敗に対応できるcreateアクション
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 def show
   @user = User.find(params[:id])
 end
 def new
   @user = User.new
 end
 def create
   @user = User.new(params[:user])    # 実装は終わっていないことに注意!
   if @user.save
     # 保存の成功をここで扱う。
   else
     render 'new'
   end
 end
end
----------------------------

renderってなんだっけな?あぁ、再描画するときにも使えるってことか!
今回の場合、newアクションを呼び出しているのね!

・7 3 2 Strong Parameters

なんかセキュリティ上の理由で昔は使えてた上記コマンド使えなくなったっぽい
今はストロングゼロみたいなやつ使うのがいいらしい

createアクションでStrong Parametersを使う
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 .
 .
 .
 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
end
----------------------------


・7 3 3 エラーメッセージ

ユーザー登録失敗時にエラーメッセージが表示されるようにする
app/views/users/new.html.erb
----------------------------
<% provide(:title, 'Sign up') %>
<h1>Sign up</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>
----------------------------

'shared/error_messages'というパーシャルをrenderする
複数のビューで使われるパーシャルは専用のディレクトリsharedによく置かれる
ただ、ディレクトリが存在しないので作る必要がある

mkdir app/views/shared

ほんでパーシャルを作っておく

フォーム送信時にエラーメッセージを表示するためのパーシャル
app/views/shared/_error_messages.html.erb
----------------------------
<% 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 %>
----------------------------

countメソッドはいくつあるか数え、any?メソッドはempty?メソッドと逆で存在していればtrue

pluralizeメソッドは、単数系なら単数形、複数形なら複数形で表示してくれる超優れもの

エラーメッセージにスタイルを与えるためのCSS
app/assets/stylesheets/custom.scss
----------------------------
.
.
.
/* forms */
.
.
.
#error_explanation {
 color: red;
 ul {
   color: red;
   margin: 0 0 30px 0;
 }
}
.field_with_errors {
 @extend .has-error;
 .form-control {
   color: $state-danger-text;
 }
}
----------------------------

・7 3 4 失敗時のテスト

フォームにテストコードが書けるので書いていこう!
rails generate integration_test users_signup

無効なユーザー登録に対するテスト green
test/integration/users_signup_test.rb
----------------------------
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
----------------------------

実は上記のコードは
before_count = User.count #数を抜き出す
post users_path, ... #異なる数
after_count = User.count #あとの数
assert_equal before_count, after_count #同じか?


・7 4 ユーザー登録成功

ユーザー登録成功時にデータベースに保存してユーザー登録フォームを完成させる

・7 4 1 登録フォームの完成

保存とリダイレクトを行う、userのcreateアクション
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 .
 .
 .
 def create
   @user = User.new(user_params)
   if @user.save
     redirect_to @user
   else
     render 'new'
   end
 end
 private
   def user_params
     params.require(:user).permit(:name, :email, :password,
                                  :password_confirmation)
   end
end
----------------------------


・7 4 2 flash

一時的に利用するもの

ユーザー登録ページにフラッシュメッセージを追加する
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
 .
 .
 .
 def create
   @user = User.new(user_params)
   if @user.save
     flash[:success] = "Welcome to the Sample App!"
     redirect_to @user
   else
     render 'new'
   end
 end
 private
   def user_params
     params.require(:user).permit(:name, :email, :password,
                                  :password_confirmation)
   end
end
----------------------------
flash変数の内容をWebサイトのレイアウトに追加する
app/views/layouts/application.html.erb
----------------------------
<!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>
----------------------------


これでユーザーが作成完了時にWelcome to the Sample App!って出るようになった!
ちなみにF5で消えるようになった!

・7 4 3 実際のユーザー登録

データベースを初期化する!
rails db:migrate:reset

ログ
----------------------------
ec2-user:~/environment/sample_app (sign-up) $ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20200101042458 CreateUsers: migrating ======================================
-- create_table(:users)
  -> 0.0008s
== 20200101042458 CreateUsers: migrated (0.0013s) =============================
== 20200101133834 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
  -> 0.0008s
== 20200101133834 AddIndexToUsersEmail: migrated (0.0013s) ====================
== 20200101152629 AddPasswordDigestToUsers: migrating =========================
-- add_column(:users, :password_digest, :string)
  -> 0.0006s
== 20200101152629 AddPasswordDigestToUsers: migrated (0.0011s) ================
ec2-user:~/environment/sample_app (sign-up) $ 
----------------------------

そしてサーバーを再起動する

Rails Tutorial
example@railstutorial.org
aaaaaaaa
aaaaaaaa

ログ
----------------------------
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
 controller: users
 action: show
 id: '1'
permitted: false
----------------------------

・7 4 4 成功時のテスト


有効なユーザー登録に対するテストgreen
test/integration/users_signup_test.rb
----------------------------
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
 .
 .
 .
 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
end
----------------------------


・7 5 プロのデプロイ

まずはマージ

・7 5 1 本番環境でのSSL

本番環境ではSSLを使うように修正する
config/environments/production.rb
----------------------------
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
----------------------------


・7 5 2 本番環境用のWebサーバー

本番環境のWebサーバー設定ファイル
config/puma.rb
----------------------------
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が使うようにProcfileで定義する
./Procfile
----------------------------
web: bundle exec puma -C config/puma.rb
----------------------------

https://jun7.herokuapp.com/


・7 5 3 本番環境へのデプロイ

いつもの
プッシュしてからマイグレーション
SSLの設定がきちんとしてるから鍵付きになってる

・7 6 最後に

ユーザー登録まで終わったよって言われてる
次からログインとログアウトについてやるよ

・7 6 1 本章のまとめ

debugメソッドを使うことで、役立つデバッグ情報を表示できる
Sassのmixin機能を使うと、CSSのルールをまとめたり他の場所で再利用できるようになる
Railsには標準で3つ環境が備わっており、それぞれ開発環境(development)、テスト環境(test)、本番環境(production)と呼ぶ
標準的なRESTfulなURLを通して、ユーザー情報をリソースとして扱えるようになった
Gravatarを使うと、ユーザーのプロフィール画像を簡単に表示できるようになる
form_forヘルパーは、ActiveRecordのオブジェクトに対応したフォームを生成する
ユーザー登録に失敗した場合はnewビューを再描画するようにした。その際、ActiveRecordが自動的に検知したエラーメッセージを表示できるようにした
flash変数を使うと、一時的なメッセージを表示できるようになる
ユーザー登録に成功すると、データベース上にユーザーが追加、プロフィールページにリダイレクト、ウェルカムメッセージの表示といった順で処理が進む
統合テストを使うことで送信フォームの振る舞いを検証したり、バグの発生を検知したりできる
セキュアな通信と高いパフォーマンスを確保するために、本番環境ではSSLとPumaを導入する

感想:テスト周りがあまり理解できてない。詳しい説明まで載っている訳ではないしとりあえずこのまま進む!


この記事が気に入ったらサポートをしてみませんか?