techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

Rails Tutorial 5 Sign up

Rails Tutorial 5 Sign up

Chapter 7/ Sign up | Ruby on Rails Tutorial (Rails 5) | Softcover.io に沿って、今回はユーザー登録機能を追加していく。
HTMLフォームを使用して登録情報をWebアプリケーションに送信し、ユーザーを新規作成して情報をデータベースに保存する。

Showing users

ユーザーの名前とプロファイル写真を表示するためのページを作成する。 最初にトピックブランチを作成する。

$ git checkout -b sign-up

アプリケーションのデータベースから取り出した情報を使用して各プロファイルの表示をカスタマイズするという、初の真に動的なページ作成となる。 準備としてデバッグ情報を追加する。

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? を入れることで、開発環境以外ではこのデバッグ情報は表示されない。

A Users resource

6章で作成したユーザ情報を、RESTアーキテクチャの慣習にしたがって表示する。
すなわち 、/users/1 にアクセスしてユーザ情報を表示できるようにする。

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

表示のためにビューファイルを作成する。
app/views/users/show.html.erb

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

ビューでは埋め込みRubyを使用し、ユーザ名とメールアドレスを表示する。ここで参照している@user変数を Users コントローラ show アクション内に定義する。
app/controllers/users_controller.rb

class UsersController < ApplicationController

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

  def new
  end
end

これでusers/1にアクセスすることでユーザ情報を表示することができるようになる。

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

これでusers/1にアクセスすると、プロンプトが表示され、変数の中身を確認したりできる。
デバッグが終わったら該当行を削除しておくだけ。

A Gravatar image and a sidebar

A Gravatar is a Globally Recognized Avatar. You upload it and create your profile just once, and then when you participate in any Gravatar-enabled site, your Gravatar image will automatically follow you there.

ということでユーザのプロフィール写真をGravatarを使って表示する。
Let's Chat でデフォルトで表示されているユーザアイコン「なんじゃこりゃ」と思っていたがこれだったのか。デファクトを知らないのが恥ずかしい。

gravatar_forをUsersコントローラに関連付けられているヘルパーファイルに置く。

app/helpers/users_helper.rb を以下のように書き換える。

module UsersHelper

  # Returns the Gravatar for the given user.
  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

上記を使用してGravatarの画像を利用できるように表示ビューを書き換える。

app/views/users/show.html.erb

<% provide(:title, @user.name) %>
<h1>
  <%= gravatar_for @user %>
  <%= @user.name %>
</h1>

これでユーザ情報のメールアドレスに紐付いた Gravatar 画像が表示されるようになる。
ここからさらにユーザーのサイドバーを作っていく。

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>

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

Signup form

ユーザ情報を表示することができるようになったので、今度はユーザ登録フォームを作成していく。

ユーザー登録ページで重要な点は、ユーザー登録に欠かせない情報を入力するためのformです。Railsでform_forヘルパーメソッドを使用します。このメソッドはActive Recordのオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築します。

なんと便利な。

@user 変数を new アクションに追加する。
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>

また、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;
}

Unsuccessful signups

無効なデータ送信に対してエラーの一覧を表示する。
間違った送信をして何も表示されなければユーザが困惑する原因となる。

マスアサインメント脆弱性対策に Strong Parameters を使うことなどが解説されている。

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する。
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 %>

エラーメッセージにスタイルを与えるための id や、単数形、複数形を適切に変更してくれるヘルパーpluralizeなどが盛り込まれている。

エラーメッセージにスタイルを与えるためのCSS id error_explanationを追記する。
app/assets/stylesheets/custom.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;
  }
}

これでエラーが表示されるようになる。

A test for invalid submission

Rails ではフォーム用のテストを書いて自動化が可能。
まずユーザ新規登録の統合テストを作成する。

$ rails generate integration_test users_signup
Running via Spring preloader in process 2839
      invoke  test_unit
      create    test/integration/users_signup_test.rb

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'
    assert_select 'div#error_explanation'
    assert_select 'div.field_with_errors'
  end
end

以下を実行してあるので上記を保存した時点で自動テストが走り、成功する。

$ bundle exec guard

上記テストでは、無効なユーザ登録リクエスト前後で登録されているユーザ数が変化していないことを確かめている。
更に、セレクタを使って特定のHTMLタグが存在するかどうかをテストしている。(エラーメッセージが意図通り表示されていることのテスト)
演習で名前付きルートと、デフォルトのRESTfulなルーティングについてもう少しあるが、以降の章を見る限り上記までにとどめてそれ以降は元に戻しておくのが良さそう。

Successful signups

次はユーザを細ンできるようにする。
- 正常なリクエストならユーザを保存し、ユーザ情報の表示とウェルカムメッセージの表示 - そうでなければ先ほど作成したエラーメッセージの表示

となるよう実装していくとのこと。

app/controllers/users_controller.rb に以下を追記する。

      redirect_to @user

するとredirect_to @userというコードから、Rails がuser_url(@user)というコードを推察してくれる。なんと便利な。

The flash

ユーザ登録完了時に一度だけ表示し、次回以降表示しない、と言った機能を実現するためにRailsでは flash という変数を使えばいいとのこと。 これまた便利。

app/controllers/users_controller.rb に以下を追記する。

    if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else

app/views/layouts/application.html.erb にいかを追記することで flash の内容を表示する。

  <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>

これだけでユーザ登録後初回のみウェルカムメッセージを表示することができる、と。

The first signup

まず以下を実行して DB をリセットする。

$ rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
== 20161023011602 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0015s
== 20161023011602 CreateUsers: migrated (0.0021s) =============================

== 20161106170056 AddIndexToUsersEmail: migrating =============================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0015s
== 20161106170056 AddIndexToUsersEmail: migrated (0.0017s) ====================

== 20161109220735 AddPasswordToUsers: migrating ===============================
-- add_column(:users, :password_digest, :string)
   -> 0.0010s
== 20161109220735 AddPasswordToUsers: migrated (0.0012s) ======================

サーバを起動してユーザ登録を行ってみる。

f:id:kfurue:20170112090317p:plain

ユーザ表示ページにウェルカムメッセージが表示されていることが確認できた。
リロードしても次回以降はウェルカムメッセージが表示されないことも確認。

A test for valid submission

ここでユーザ登録成功パターンのテストを追加する。

test/integration/users_signup_test.rb に以下を追記する。

  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'
    assert_not flash.empty?
  end
  • 登録後に、登録前よりユーザ数が1増加していること
  • Userのshowアクションが正しく動作していること
  • show.html.erbビューが正しく動作していること
  • flash が空でないこと

が確認できる。

00:16:08 - INFO - Running: test/integration/site_layout_test.rb
Running via Spring preloader in process 5607
Started with run options --seed 46326

  19/19: [============================================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.03830s
19 tests, 40 assertions, 0 failures, 0 errors, 0 skips

実行結果は上記の通り、すべて成功。

Professional-grade deployment

ここでデプロイを進めるが、今回からメールアドレスやらパスワードやらのやりとりが入るので SSL 対応を入れ込む、とのこと。

config/environments/production.rb が本番環境設定なので、こちらに以下の行のコメントアウトを解除する。

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

Heroku のサブドメインで動作させているうちは、なんとこれだけで SSL 対応完了だ。便利

独自ドメインで SSL 対応させたければ以下を参考に作業すれば良いらしい。
Heroku SSL | Heroku Dev Center

また、Webサーバも本番環境向けに変更する。 - デフォルトでは WEBrick - 構築が簡単 - 反面、多数のリクエストを捌くのは苦手 - 本番環境ではPuma に変更する - 多数のリクエストを捌くことができる - しかも Rails 5 からは Puma を使用するために必要な設定が簡単になっている

ありがたや。

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

./Procfile を作成し、以下を記載する。

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

Heroku にデプロイすれば SSL で動作するようになる。

その他

Rebuild 聞きながらやってたらそっちが面白くてすぐに手が止まる。

Rebuild: 169: Your Blog Can Be Generated By Neural Networks (omo)

  • 本読んだ感想として、「新人に読ませたい」じゃなくてあなたはどう思ったの
  • 「達人プログラマー」は精神論的な部分と技術的な部分の2パート構成
    • 前者はいいが、後者が陳腐化しつつある

みたいな感じで対談が進んでいく。
「達人プログラマーをディスる」との宣言で始まったが根底に流れるのはこの本への愛だ(と勝手に解釈した)。

精神論がキリッとしているのにその実例がしょぼいというのは良くない。ちゃんと書き直さないといけない。誰かが。

たしかに、せっかく新装版出たのにな、というところではありますが。
精神論的な部分はちょっとまとめて社内でも共有してみようかな。