Rails5「中級」チュートリアル(3-1)投稿機能: 認証(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails5中級チュートリアルはセットアップが短めで、RDBMSにはPostgreSQL、テストにはRSpecを用います。
原文が非常に長いので分割します。章ごとのリンクは順次追加します。

注意: Rails中級チュートリアルは、Ruby on Railsチュートリアル(https://railstutorial.jp/)(Railsチュートリアル)とは著者も対象読者も異なります。

目次

Rails5「中級」チュートリアル(3-1)投稿機能: 認証(翻訳)

3 投稿機能

そろそろ投稿機能の実装に取りかかれるようになってきました。このアプリの目的は、ユーザーが自分の好みに近い人に出会えるようにすることなので、投稿の著者を識別できるようにしなければなりません。これを行うには認証システムが必要です。

3-1 認証

認証システムにはDevise gemを使うことにします。認証システムを自作することもできなくはありませんが、相当頑張らないといけないでしょう。本チュートリアルでは楽な道を進むことにします。かつDeviseはRailsコミュニティで人気の高いgemでもあります。

まずは新しいブランチを切りましょう。

git checkout -b authentication

他のgemと同様、Deviseのセットアップはドキュメントに従って進めます。幸いなことに、セットアップは非常に簡単です。

Gemfileに以下を追加します。

gem 'devise'

以下のコマンドを実行します。

bundle install
rails generate devise:install

実行後、コマンドプロンプトにいくつか指示が表示されます。本チュートリアルではmailerを使わないので、追加の設定は不要です。

もしRailsのモデルについてまったく知識がないのであれば、RailsガイドのActive Recordの基礎Active Modelの基礎を取り急ぎ読んで、ActiveRecordやActiveModelに慣れておく必要があります。

それでは、DeviseのジェネレータでUserモデルを作成しましょう。

rails generate devise User

以下を実行してアプリのデータベースを初期化します。

rails db:create

続いて、以下を実行してデータベースに新しいテーブルを作成します。

rails db:migrate

以上で認証システムの技術面のセットアップは終わりです。これで、Deviseが提供するメソッドを用いて新しいユーザーを作成できるようになりました。変更をcommitしましょう。

git add -A
git commit -m "Add and configure the Devise gem"

Devise gemをインストールすると、バックエンドの機能の他にデフォルトのビューも使えるようになります。以下を実行してルーティングを表示してみましょう。

rails routes

さっきまではrootへのルーティングしかなかったのに、新しいルーティングがたくさんできているのがわかります。もしわからなくなりそうだったら、いつでもDevise wikiのドキュメントを開いて情報を見つけることができます。自分と同じ問題で悩んだ人はたくさんいるはずなので、ググって答えを見つけられる可能性はかなりあります。

参考: Devise How-To wiki日本語目次もどうぞ。

ルーティングをいくつか試してみましょう。localhost:3000/users/sign_inをブラウザで開くと、以下のようなサインインページが表示されるはずです。

localhost:3000/users/sign_upを開いてみると、同じようなページが表示されます。しかしviewsディレクトリを見てみると、どこにもDeviseのディレクトリがありません。どこをいじればよいのでしょうか?Noob Noobならここで「Got Damn!」というところです。Deviseのドキュメントに記載されているように、Deviseのビューを変更するにはジェネレータを使ってビューを生成する必要があります。以下を実行します。

rails generate devise:views

viewsディレクトリを見てみると、Devise用のディレクトリが生成されているのがわかります。ここでサインアップページやログインページの表示方法を変更できます。このアプリではログインページの実装の方が素直になるので、まずはログインページから変更します。この登録ページに機能を追加したいので、少し手間をかける必要があります。

ログインページ

app/views/devise/sessions/new.html.erbを開きます。

ログインページのビューはこのファイルに保存されており、その実体は単なるログインフォームです。このフォームの生成に[form_for](http://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/FormHelper.html)メソッドが使われていることにお気づきでしょうか。form_forはフォームの生成に便利なRailsのメソッドです。このフォームのスタイルを変更してBootstrapを適用することにします。このファイルの内容を以下で置き換えます(Gist)。

<!-- views/devise/sessions/new.html.erb -->
<%= bootstrap_form_for(resource,
                       as: resource_name,
                       url: session_path(resource_name)) do |f| %>

    <%= f.email_field :email,
                      autofocus: true,
                      class: 'form-control',
                      placeholder: 'email' %>

    <%= f.password_field  :password,
                          autocomplete: "off",
                          class: 'form-control',
                          placeholder: 'password' %>


  <% if devise_mapping.rememberable? -%>
     <%= f.check_box :remember_me %>
  <% end -%>

   <%= f.submit "Log in", class: 'form-control login-button' %>
<% end %>

特別なことは何もしていません。このフォームをBootstrapのフォームに変えるためにメソッド名をbootstrap_form_forに変更し、form-controlクラスをフィールドに追加しているだけです。

このメソッドの引数の書き方を見てみましょう。各引数を1行ずつ分けて書いてあります。このように書いた理由は、コードが横に長くなるのを避けるためです。読みやすさのため、コード行は最大80文字を超えないようにするのが普通です。本チュートリアルでは今後もこのスタイルでコードを書くことにします。

この時点でlocalhost:3000/users/sign_inをブラウザで開くと、以下のエラーが表示されます。

undefined method 'bootstrap_form_for'

RailsでBootstrapのフォームを使うには、bootstrap_form gemの追加が必要です。Gemfileに以下を追加します。

gem 'bootstrap_form'

続いて以下を実行します。

bundle install

これで、以下のようにログインページが表示されるはずです。

変更をcommitします。

git add -A
git commit -m "Generate devise views, modify sign in form
and add the bootstrap_form gem."

Bootstrapのグリッドシステムをページに適用するには、以下のようにログインフォームをBootstrapのコンテナでラップします(Gist)。

<!-- views/devise/sessions/new.html.erb -->
<div class="container">
  <div class="row">
    <div class="col-sm-6 col-sm-offset-3">
      <h2 class="text-center">Log in</h2>

      <!-- 上のログインフォームをここに貼る -->

    </div>
  </div>
</div>

このログインフォームの幅は12カラムのうち6カラムを使い、オフセットは3カラムにします。画面の小さなデバイスではフル画面の幅でフォームが表示されます。Bootstrapのグリッドはこのように動作します。

ここでさらにcommitしておきましょう。変更量が少ないのではないかとお思いかもしれませんが、私は普段このぐらいの粒度でcommitしています。私は、ある部分に明確な変更を実装したらcommitするようにしています。こうすることで変更が追いやすくなり、コードの移り変わりも理解しやすくなると私は思います。

git add -A
git commit -m "wrap login form in the login page with a boostrap container"

ログインページにアクセスするURLを、/users/sign_inから/loginに変えるとよさそうです。それにはルーティングの変更が必要ですが、そのためにアクションの場所を知っておく必要があります。ログインページにアクセスすると、その場所にあるアクションを呼び出します。Deviseのコントローラは、Devise gem自身の中にあります。Deviseのドキュメントを読むと、Deviseのコントローラはすべてdeviseディレクトリの下にあることがわかります。正直、大した発見ではありませんが(U_U)。ルーティングの単純な変更はdevise_scopeメソッドで行えます。routes.rbファイルを開いて以下を追加します。

devise_scope :user do
  get 'login', to: 'devise/sessions#new'
end

変更をcommitします。

git add -A
git commit -m "change route from /users/sign_in to /login"

ログインページはとりあえずこのままにしておきましょう。

サインアップページ

localhost:3000/users/sign_upをブラウザで開くと、こちらはDeviseのデフォルトのサインアップページのままになっています。先ほど申し上げたとおり、このページには少し手間をかける必要があります。usersテーブルに:nameカラムを追加して、Userオブジェクトで:name属性を扱えるようにするためです。

schema.rbファイルに少し変更を加えます。スキーマの変更やマイグレーションがよくわからない方は、ガイドのActive Recordマイグレーションをひととおり読んでおくことをおすすめします。

最初に、usersテーブルにカラムを追加しなければなりません。これは、マイグレーションファイルを新しく作成し、change_tableメソッドでカラムを追加することで行えます。しかし開発はまだ始まったばかりなので、アプリはまだデプロイされていません。そこで、既存のdevise_create_usersマイグレーションファイルにカラムを追加してデータベースを再作成することにします。db/migrateディレクトリに移動して(作成日)_devise_create_users.rbファイルを開き、create_tableの内側にt.string :name, null: false, default: ""を追加します。

以下のコマンドを実行して、データベースの削除と再作成を行ってから、マイグレーションを実行します。

訳注: データベース操作中はRailsサーバーを停止しておきましょう。

rails db:drop
rails db:create
rails db:migrate

これでusersテーブルにカラムが追加され、schema.rbファイルが変更されました。

追加した属性を送信できるようにするには、Deviseのコントローラで属性を受信できるよう、コントローラレベルの変更を少々行わなければなりません。Deviseコントローラの変更方法はいくつか考えられます。Deviseのジェネレータでコントローラを生成する方法や、ファイルを1つ作成して変更したいコントローラやメソッドをそこで指定する方法があります。どちらの方法でも構いませんが、ここでは後者の方法にします。

app/controllersディレクトリに移動してregistrations_controller.rbファイルを作成し、以下のコードをファイルに追加します(Gist)。

# controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  private

  def sign_up_params
    params.require(:user).permit( :name,
                                  :email,
                                  :password,
                                  :password_confirmation)
  end

  def account_update_params
    params.require(:user).permit( :name,
                                  :email,
                                  :password,
                                  :password_confirmation,
                                  :current_password)
  end
end

上のコードによってsign_up_paramsメソッドとaccount_update_paramsメソッドがオーバーライドされ、:name属性を受信できるようになります。見てのとおり、2つのメソッドはDeviseのRegistrationsControllerクラスにあるので、このクラスを指定してメソッドを変更しました。2つのメソッドがオーバーライドされるよう、このコントローラをルーティングの内部で指定しなければなりません。route.rbで以下の変更を行います。

devise_for :users

上を以下のコードに置き換えます。

devise_for :users, :controllers => {:registrations => "registrations"}

変更をcommitします。

git add -A
git commit -m "
- Add the name column to the users table.
- Include name attribute to sign_up_params and account_update_params
  methods inside the RegistrationsController"

次にnew.html.erbファイルを開きます。

app/views/devise/registrations/new.html.erb

今度も同様に、フォーム以外のコードをすべて削除し、フォームをBootstrapのフォームに変えます。今回はnameフィールドを追加します(Gist)。

# views/devise/registrations/new.html.erb
<%= bootstrap_form_for(resource,
                       :as => resource_name,
                       :url => registration_path(resource_name)) do |f| %>

  <%= f.text_field :name,
               placeholder: 'username (will be shown publicly)',
               class: 'form-control' %>
  <%= f.text_field :email,
               placeholder: 'email',
               class: 'form-control' %>
  <%= f.password_field :password,
                       placeholder: 'password',
                       class: 'form-control' %>
  <%= f.password_field :password_confirmation,
                       placeholder: 'password confirmation',
                       class: 'form-control' %>
  <%= f.submit 'Sign up', class: 'btn sign-up-button' %>
<% end %>

変更をcommitします。

git add -A
git commit -m "
Delete everything from the signup page, except the form.
Convert form into a bootstrap form. Add an additional name field"

フォームをBootstrapコンテナでラップし、テキストを追加します(Gist)。

# views/devise/registrations/new.html.erb
<div class="container" id="sign-up-form">
  <div class="row">
    <h1>Get in touch with like-minded people</h1>
    <h3>Create, study, accomplish goals together</h3>

    <div class="col-sm-offset-4 col-sm-4">
      <h3>Sign up <small>it's free!</small></h3>

        <!-- 上のフォームをここに貼り付ける -->

    </div>
  </div>
</div>

変更をcommitします。

git add -A
git commit -m "
Wrap the sign up form with a bootstrap container.
Add informational text inside the container"

ログインページのときと同様、サインアップページのURLもusers/sign_upから/signupに変えるのがよさそうです。routes.rbファイルに以下のコードを追加します。

devise_scope :user do
  get 'signup', to: 'devise/registrations#new'
end

変更をcommitします。

git add -A
git commit -m "Change sign up page's route from /users/sign_up to /signup"

先に進む前に、スタイルを少し変更しておきましょう。app/assets/sytlesheets/partialsに移動してsignup.scssファイルを作成し、以下のCSSコードを追加します(Gist)。

// assets/stylesheets/partials/signup.scss
#sign-up-form {
  margin-top: 100px;
  h1 {
    font-size: 36px !important;
    font-size: 3.6rem !important;
  }
  text-align: center;
  padding-bottom: 20px;
}

application.scssファイル内でpartialsからファイルをimportしていないので、ここでやっておきましょう。application.scssファイルを開いて、@import partials/layout/*のすぐ上の行でpartialsディレクトリからすべてのファイルをimportします。変更後のapplication.scssは次のようになります(Gist)。

// assets/stylesheets/application.scss
// Partials - メインのcssファイル
@import "partials/*";
@import "partials/layout/*";

変更をcommitします。

git add -A
git commit -m "
- Create a signup.scss and add CSS to the sign up page
- Import all files from partials directory to the application.scss"

Webサイト全体の外観に別の変更を加えます。app/assets/stylesheets/baseディレクトリに移動してdefault.scssを作成し、以下のCSSコードを追加します(Gist)。

// assets/stylesheets/base/default.scss
* {
  box-sizing: border-box;
}

html {
  font-size: 62.5%;
}

body {
  background: $backgroundColor;
  font-size: 14px;
  font-size: 1.4rem;
}

h1 {
  font-size: 24px;
  font-size: 2.4rem;
}

i {
  width: 26px;
}

ul {
  list-style-type: none;
}

a:hover, a:active, a:link, a:visited {
  text-decoration: none;
}

.control-label {
  display: none;
}

上ではWebサイト全体に一般的なスタイル変更をいくつか適用しています。font-size62.5%に設定することで、単位1 rem10pxを表すようにします。rem単位がよくわからない場合は、こちらのチュートリアルをご覧ください。Bootstrapフォームでラベル文字を表示したくないので、以下をCSSで設定しています。

.control-label {
  display: none;
}

$backgroundColor変数が使われていることにお気づきでしょうか。しかしこの変数はまだ設定されていないので、variables.scssファイルを開いて以下を追加します。

$backgroundColor: #f0f0f0;

default.scssファイルがapplication.scssでまだimportされていないので、「// Variables」の下にimportを追加します。追加後のapplication.scssファイルは次のようになります。

// assets/stylesheets/application.scss
...

// Variables
@import "base/variables";

// Default styles
@import "base/default";

...

変更をcommitします。

git add -A
git commit -m "
Add CSS and import CSS files inside the main file

- Create a default.scss file and add CSS
- Define $backgroundColor variable
- Import default.scss file inside the application.scss"
ナビゲーションバーの更新

ここまでに、homeページ、ログインページ、サインアップページを作りました。このあたりで、これらのページを互いにつないでユーザーがWebサイトを楽に操作できるようにしましょう。ナビゲーションバーにサインアップページとログインページへのリンクを置くことにします。_navigation.html.erbファイルを開きます。

app/views/layouts/_navigation.html.erb

ここにコードを少し追加することにします。ここには今後さらにコードを追加する予定です。このままだとファイルのコード量が増え、管理やテストがやりにくくなってしまいます。長いコードを扱いやすくするために、もっと短いコード(チャンク)に分割することにします。本チュートリアルではパーシャルを使って行います。コードを追加する前に、現在の_navigation.html.erbのコードをパーシャルに切り出してみましょう。

ここで、アプリのナビゲーションバーの使われ方について簡単に説明しておきます。ナビゲーションバーを2つに分け、一方は画面サイズにかかわらず常に表示し、他方は大画面のときだけ表示して小画面では隠すようにします。

.container要素の内部は以下のような構造になる予定です(Gist)。

<!-- layouts/_navigation_html.erb -->
<div class="row">

  <!-- 常に表示する要素 -->
  <div class="col-sm-7">
  </div><!-- col-sm-7 -->

  <!-- 小画面デバイスで隠す要素 -->
  <div class="col-sm-5">
  </div><!-- col-sm-5 -->

</div><!-- row -->

app/views/layoutsディレクトリの下にnavigationディレクトリを作成し、その下に_header.html.erbパーシャルファイルを作成します。

app/views/layouts/navigation/_header.html.erb

_navigation.html.erbファイルの.navbar-headerセクションをまるごとカットして_header.html.erbファイルに貼り付けます。navigationディレクトリの下に別の_collapsible_elements.html.erbパーシャルファイルを作成します。

app/views/layouts/navigation/_collapsible_elements.html.erb

_navigation.html.erbファイルの.navbar-collapseセクションをまるごとカットして_collapsible_elements.html.erbファイルに貼り付けます。今度は2つのパーシャルを_navigation.html.erbファイルの内部でレンダリングしましょう。変更後のファイルは以下のようになります。

<!-- layouts/_navigation_html.erb -->
<nav class="navbar navbar-default navbar-fixed-top">
  <div class="container">
    <div class="row">

      <!-- 常に表示する要素 -->
      <div class="col-sm-7">
        <%= render 'layouts/navigation/header' %>
      </div><!-- col-sm-7 -->

      <!-- 小画面デバイスで隠す要素 -->
      <div class="col-sm-5">
        <%= render 'layouts/navigation/collapsible_elements' %>
      </div><!-- col-sm-5 -->

    </div><!-- row -->
  </div><!-- container -->
</nav>

この時点でhttp://localhost:3000をブラウザで表示しても、表示結果は何も変わっていません。コードを少し整理して開発を進める準備を整えただけです。

ナビゲーションバーにリンクを追加する準備が整いました。_collapsible_elements.html.erbファイルを再度開きます。

app/views/layouts/_collapsible_elements.html.erb

このファイルにリンクを追加します。置き換え後のファイルは以下のようになります。

<!-- layouts/navigation/_collapsible_elements.html.erb -->
<!-- ナビゲーションリンク/フォームなどのコンテンツをここにまとめて表示をオンオフできるようにする -->
<div class="collapse navbar-collapse navbar-right" id="navbar-collapsible-content">
  <ul class="nav navbar-nav ">
    <% if user_signed_in? %>
      <li class="dropdown pc-menu">
        <a id="user-settings" class="dropdown-toggle" data-toggle="dropdown" href="#">
          <span id="user-name"><%= current_user.name %></span>
          <span class="caret"></span>
        </a>

        <ul class="dropdown-menu" role="menu">
          <li><%= link_to 'Edit Profile', edit_user_registration_path %></li>
          <li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li>
        </ul>
      </li>

      <li class="mobile-menu">
        <%= link_to 'Edit Profile', edit_user_registration_path %>
      </li>
      <li class="mobile-menu">
        <%= link_to 'Log out', destroy_user_session_path, method: :delete %>
      </li>

    <% else # ユーザーがサインインしていない場合 %>
      <li ><%= link_to 'Login', login_path %></li>
      <li ><%= link_to 'Signup', signup_path %></li>
    <% end # ユーザーがサインインした場合 %>
  </ul>
</div><!-- navbar-collapse -->

ここでコードの変更点について少し補足します。最初に、2行目で要素のidnavbar-collapsible-contentに変更しました。これはBootstrapの機能で、コンテンツを折りたたみ可能にするために必要です(デフォルトのidbs-example-navbar-collapse-1)。この機能をトリガするために、data-target属性を持つボタンが_header.htmlファイルにあります。views/layouts/navigation/_header.html.erbファイルを開いて、data-target属性をdata-target="#navbar-collapsible-content"に変更します。これで、ボタンも折りたたみ可能になりました。

次に、_collapsible_elements.html.erbif/elseロジックがいくつかあり、そこにDeviseのuser_signed_in?メソッドがあります。これは、ユーザーがサインインしているかどうかでリンクを変更するためのものです。ビューのこうしたif/elseロジックを放置するのはよくありません。ビューは判断を一切行わず、ひたすら情報を吐き出す「静的な」記述に徹するべきです。このロジックは後ほどヘルパーを使ってリファクタリングする予定です。

最後に、このファイルにpc-menumobile-menuという2つのCSSクラスがあります。これらのクラスは、画面サイズが変わったときのリンクの表示を制御するのに使います。これらのクラスのCSSを追加してみましょう。app/assets/stylesheetsディレクトリに移動してresponsiveディレクトリを作成し(訳注: このディレクトリは前の手順で作成済みです)、そこにdesktop.scssファイルとmobile.scssファイルを作成します。Scssファイルを2つ作るのは、画面サイズごとに設定を変えるためです。desktop.scssファイルに以下を追加します(Gist)。

// assets/styleshseets/responsive/desktop.scss
@media screen and (min-width: 767px) {
  .mobile-menu {
    display: none !important;
  }
}

mobile.scssファイルに以下を追加します(Gist)。

// assets/styleshseets/responsive/mobile.scss
@media screen and (max-width: 767px) {
  .pc-menu {
    display: none !important;
  }
}

CSSのメディアクエリがよくわからない方はこちらをお読みください。
次に、responsiveディレクトリの内容をapplication.scssでimportします。以下のようにファイルの末尾にimportを追加します(Gist)。

// app/assets/stylesheets/application.scss
// メディアクエリ(レスポンシブデザイン用)
@import "responsive/*";

次はnavigation.scssファイルを開きます。

app/assets/stylesheets/partials/layout/navigation.scss

このファイルのnav要素セレクタの内側に以下を追加して、ナビゲーションバーのスタイルを微調整します(Gist)。

// assets/styleshseets/partials/layout/navigation.scss
.col-sm-5, .col-sm-7 {
  padding: 0;
}

続いて、nav要素の外に以下のCSSコードを追加します(Gist)。

// assets/styleshseets/partials/layout/navigation.scss
.pc-menu {
  margin-right: 10px;
}

.mobile-menu {
  i {
    color: white;
  }
  ul {
    padding: 0px;
  }
  a {
    display: block;
    padding: 10px 0px 10px 25px !important;
  }
  a:hover {
    background: white !important;
    color: black !important;
    i {
      color: black;
    }
  }
}

.icon-bar {
  background-color: white !important;
}

.active a {
  background: $navbarColor !important;
  border-bottom: solid 5px white;
}

.dropdown-toggle, .dropdown-menu {
  background: $navbarColor !important;
  border: none;
}

.dropdown-menu a:hover {
  color: black !important;
  background: white !important;
}

ここまでを終えると、アプリにユーザーがログインしていないときの外観は以下のようになっているはずです。

アプリにユーザーがログインしたときの外観は以下のようになります。

画面サイズを小さくしたときの外観は以下のようになります。

変更をcommitします。

git add -A
git commit -m "
Update the navigation bar

- Add login, signup, logout and edit profile links on the navigation bar
- Split _navigation.scss code into partials
- Create responsive directory inside the stylesheets directory and add CSS.
- Add CSS to tweak navigation bar style"

これで基本的な認証機能ができあがり、要求を満たせました。今度はauthenticationブランチをmasterブランチにmergeしましょう。

git checkout master
git merge authentication

今度も変更の概要が表示されます。authenticationブランチが不要になったので削除します。

git branch -D authentication

関連記事

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ