Pythonとサラリーマンと

2020年6月にPythonを始めたサラリーマンのブログです。

Rails Tutorial 第7章を噛み砕く

Rails Tutorial 第7章で学ぶこと

  • 第6章で準備したテーブルを使って、実際にユーザ登録するページを作る
  • 入力フォームに許可していない値を入力させない工夫もする

ようやくユーザ登録ページの作成。 ここでは主にコントローラとビューを作成する。 自作のアプリを作るときもモデルの定義/テーブルの作成を完了してから、コントローラ/ビューを作成するのかな。

この記事は、筆者のRails Tutorial 第6章に関する備忘録。
実際に手を動かす時はRails Tutorialの流れに沿って行うこと。
なお、テストに関する記載は省いている。

デバッグ情報の出力方法①

<%= debug(params) %>

ビューにこれを差し込めばデバッグ情報を出力してくれる。 (params)という記述することで呼び出したコントローラとアクションを表示してくれる様子。

<%= debug(params) if Rails.env.development %>

こうすれば開発環境の時だけデバッグ情報を出力してくれる。(本番環境では出力させない)
ということで、実際に差し込んでみよう。
app/views/layouts/application.html.erb

  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>

次に、デバッグ情報を綺麗に表示するためにcustom.csccをいじる。

@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;
}

@mixinというのはメソッドみたいなもの。 box_sizingというmixinをここで作っている。 一度準備したmixinは@include box_sizingという記述で何度でも呼び出せる。

こんな感じでサンプルアプリのどのページでもデバッグ情報を表示できるようになった。
*筆者は、この時点でdebug情報の有効性を分かっていない

Userに関するルーティングを一発解決

resources :users

routes.rbファイルにこの一行を記述するだけでuserコントローラに関するルーティングは完了。 HTTPリクエスト内容とURLに応じて、適切なコントローラ・アクションを呼び出してくれる。
*HTTPリクエスト→GET/POST/PATCH/DELETE

  • 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 + /user/1/edit = editアクション edit_user_path(user)
  • PATCH + /users/1 = updateアクション user_path(user)
  • DELETE + /users/1 = updateアクション user_path(user)

ユーザ個人のページ作成

userコントローラは既に作ってあるので以下のアクションを追記。
app/controllers/users_controller.rb

  def show
    @user = User.find(params[:id])
  end

そしてviewsのusersディレクトリにshow.html.erbを作成する。
↓とりあえず適当なhtmlを記載する。
app/views/users/show.html.erb

<%= @user.name %>, <%= @user.email %>

showアクションで@userというインスタンス変数を作成し、idをキーにデータを取り出しているので@user.name/ @user.emalにはidに応じた値が代入される。

デバッグ情報の出力方法②

byebug gem。このgemのdebuggerメソッドを使えばdebugメソッドよりも直感的にデバッグすることができる。
使い方は、、、

  • 「あ、なんかこのページ表示しようとするとエラーが出る」
  • コントローラの中で問題が起こってそうなコードの付近にdebuggerを挿入
  • 当該ページに再度アクセス
  • Railsサーバを立ち上げたターミナルにbybugプロンプトが表示されている
  • その時点でインスタンス変数にどのような値が代入されているかを@user.nameなどで確認できる
  • デバッグが終わったらdebuggerを削除する

↓これがdebuggerの差し込み方。

  def show
    @user = User.find(params[:id])
    debugger
  end

この状態で~.com/users/1というアクセスを行うとターミナル上でbyebugプロンプトが立ち上がっている。
それを上手く使いこなせばデバッグを行えるということ。
*筆者がcloud9上で行ったところbyebugプロンプトは重くて使い物にならなかった。
*debuggerというものが存在するということを知っただけで、今はまだ使い方は分からなくて良いと考える。

ユーザの画像を簡単に取り扱うツール

無料サービスの"Gravatar"を使えば

  • Gravaterにプロフィール写真をアップロードできる
  • プロフィール画像とメールアドレスを簡単に関連づけられる
  • ビューにGravater専用の画像パスを記載すれば良い感じに表示できる

ということでGravaterを使ってユーザーのプロフィール画像を表示できるようにしよう。
↓まずはGravaterメソッドをヘルパーに作成する。usersコントローラに関連づけられたヘルパーとする。
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

これでusersコントローラでGravaterメソッドを使えるようになった。

Gravaterメソッドの解説
GravaterのURLにメールアドレスのパラメータを渡してあげれば登録している画像を持ってこれる。
ただし、Gravater上では登録されたメールアドレスがMD5という仕組みでハッシュ化されて保存されている。
そのため@userのメールアドレスをMD5ハッシュ化してGravaterのURLへパラメータとして渡してあげる必要がある。
RubyではDigestというライブラリのhexdigestメソッドを使えばMD5形式のハッシュ化を行うことができる。
なお、MD5では大文字・小文字が区別されるのでdowncaseメソッドで小文字にしている。
最終的に表示を行っっているのはimage_tag。gravater classとaltテキストも追加しておく。

↓次に、showビューにgravaterメソッドの記載と、asideによるサイドバー化を記載する。
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>

↓最後にcssに追記をしてページにスタイルを与える。
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;
}

これで登録されたユーザ毎の固有のユーザページを表示できるようになった。

ユーザ登録ページの作成

ユーザ登録ページに欠かせないのがform_forメソッド。
これを使えば入力フォームを簡単に作ることができる。

↓ひとまずform_forに使用させるインスタンス変数を作る。
app/controllers/users_controller.rb

  def new
    @user = User.new
  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>

↓フォームのスタイルの追加。
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;
}

これでユーザページが完成した。
form_forは本来htmlで書くべき内容をシンプルな記述だけで済むようにしてくれている。
その他にも気が利いたことそしているっぽいので別途学習した方が良さげ。
submitをクリックされると入力値をpostで送信する仕組みを構築してくれている。

createアクションで使用するテクニック

form_forによってpost送信された値はcreateアクションで処理される。 createアクションの完成系は以下。
app/controllers/users_controller.rb

  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

ここにはいくつかのテクニックが使用されているので、以下で解説。

Strong Parameter

Strong Parameterというテクニックを使って、許可してない入力値を送信させないことができる。
これを使わないとURLフォームに直接パラメータを入力されて悪用される可能性がある。 つまるところ、セキュリティのためにStrong Parameterを使おう。

@user = User.new(user_params)
private
   
   def user_params
     params.require(:user),permit(:name, :email, :password, :password_confirmation)
   end

上記の2つはセット。 これでuser_paramsで許可した項目にしか値を送信できなくなった。

エラーメッセージの表示方法

createアクションでsaveを行った時、成功しなければfalseが帰ってくる。
上記のcreateアクションでfalseの場合はrender newをしているのでnewページが再度表示される。
newで帰って来た時@userはエラーメッセージを保有しているのでビューで表示させることができる。↓

<% 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してやる。
パーシャルを呼び出すためにrenderを使用している form-controlクラスはbootstrapで使えるクラス。 error_messagesというパーシャルを作る必要がある。
app/view/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 %>

error_messagesは複数のビューで共有したいのでsharedというディレクトリに置いている。
sharedは初めからあるわけじゃない。新しいディレクトリを作成する。

any?メソッド →要素が一つでもあればtrueを返す。なければfalse。(empty?の反対)
countメソッド →要素の数を数える。
pluralize →英語専用のテキストヘルパー。最初の引数に整数が与えられると、2番目の引数を複数けいにした物を返す。

エラーメッセージのスタイルに関しては、error_explanationに加えてfield_with_errorsについても定義する。
form_forではエラーになって元のページに戻った時、エラーになったフィールドをfield_with_errorsクラスをもつdivタグで囲むという動きをする。
これにスタイルを与えることで、そのフィールドを目立たせることができる。

#error_explanation {
  color: red;
  ul {
    color: red;
    margin: 0 0 30px 0;
  }
}

.field_with_errors {
  @extend .has-error;
  .form-control {
    color: $state-danger-text;
  }
}

ユーザ登録が成功したら

新しく登録されたユーザのプロフィールページに飛ばす。↓

redirect_to @user

え?@userにリダイレクト??となるが、この記述はRailsによって以下と解釈される。↓

redirect_to user_url(@user)

flashを使って「成功したよ!」を表示する

新しいユーザのプロフィールページが上記のリダイレクトによって飛ばされた時にのみ、新規ユーザへのウェルカムメッセージを載せると一段と小洒落たサイトになる。
その時に使うのがflash。 ↓まずは、これをコントローラに仕込む。

flash[:success] = "Welcome to Sample App"

↓そして、これをビューに挿入する。

<% flash.each do |message_type, message| %>
  <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

alert-successというのはbootstrapのスタイル。 bootstrapにはalert-dangerやalert-info、alert-warningが用意されている。
[:success]という部分が、なぜmessage_typeと解釈されるのか不明だが、こういうものだと思うにしかなさそう。 ↓ビューの全体像はこんな感じになる。

<!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有効にしてクライアント→サーバの通信をセキュアに

Railsではproduction.rbのファイルを1行書き換えるだけで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

ただ、これだけでSSL通信が行えるようになった訳ではない。
サーバ側でSSLが使えるようにセットアップする必要がある。
Herokuのサブドメインを使っている場合は、HerokuのSSL証明書を使える。
独自ドメインを使う時は、しっかりサーバサイドの設定を行う必要がある。

devcenter.heroku.com

本番環境用のWebサーバに変更

HerokuのデフォルトはWEBrick
WEBrickはセットアップが簡単だが、多くのトラフィックを扱うのには不向き。
多数のリクエストを捌くのに適したPumaを導入する。
導入方法はRails5では2ステップ。
Step1:ファイルの書き換え 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

Step2:Procfileの作成 (ルートディレクトリに設置) ./Procfile

web: bundle exec puma -C config/puma.rb

ここまで完了したらHerokuにデプロイして完了。

$ rails test
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push
$ git push heroku
$ heroku run rails db:migrate