rails beginner

→Railsビギナー 第0章はこちら

→Railsビギナー 第1章はこちら

→Railsビギナー 第3章はこちら

 

→Railsビギナー 免責事項 に同意いただける方のみ、教材の利用をお願いいたします。

 

deviseというgemを使って、ユーザー登録・ログイン機能を作成します。

※gemとは…Rubyのライブラリのこと。よく分からない方は、gemを使えば、機能を簡単に実装できると理解すれば十分です。

 

それでは早速、deviseを使う準備をしましょう。

Gemfileにgem ‘devise’を追加してください。

railsビギナー

group :development, :test doの上あたりに書きましょう。

 

ターミナルに下記のコマンドを入力して、gemのインストールを行います。

bundle install

もし「『bundle update』を行ってください」というメッセージがでたら、『bundle update』を行ってから『bundle install』を行ってください。

 

次にターミナルに下記コマンドを入力して、deviseの初期設定を行います。

rails g devise:install

次はdeviseを使って、userモデルを作成します。ターミナルに下記のコマンドを入力。

rails g devise user

作成されたマイグレーションファイルを確認しましょう。

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

うわっ!何かよく分からないコードがたくさん…

とりあえずこの段階では、書かれている内容を理解しなくても大丈夫です。

どうしても気になる方は、ご自身でググってください。

のちほどメール認証機能を実装するために、25~28行目と41行目のコメントアウトされている部分を修正してください。

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

修正できたら下記のコマンドをターミナルに入力。

rails db:migrate

ルーティングファイルも確認しておきましょう。

Rails.application.routes.draw do
  devise_for :users
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  get "/questions", to: "questions#index"
  get "/questions/new", to: "questions#new"
  post'/questions', to: "questions#create"
  get '/questions/:id', to: 'questions#show'
end

あれ?なんか書いた覚えのないコードが、2行目にありますね。

これはdeviseの初期設定を行った時に、自動生成されたものです。

 

これだけで新規登録・ログインできます!

まずサーバを再起動してください。

(サーバを起動させたターミナル上で、control + cをクリック。その後rails sを入力)

 

それでは「localhost:3000/users/sign_up」にアクセスして、適当なメールアドレスとパスワードを入力して新規登録しましょう。

(メールアドレスには@が必須です。パスワードは6文字以上で入力してください。)

railsビギナー

Sign upボタンを押すとトップページに遷移します。

railsビギナー

本当に新規登録されているか、railsコンソールで確認しましょう。

rails c -s
User.find(1)

上記のコマンドを入力すると、下記のような値が返ってくると思います。

=> #<User id: 1, email: “email@example”, ……(省略)……

 

登録されていると確認できましたね。

 

次はログアウトのリンクを設置してあげましょう。

ヘッダーを作り、そこにログアウトのリンクを設置します。

app/views/layouts/application.html.erbに下記のコードを追加。

<!DOCTYPE html>
<html>
  <head>
    <title>BeginnerApp</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= render 'layouts/header' %>
    <p><%= notice %></p>
    <p><%= alert %></p>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

コントローラーでrenderを使ったことはありますが、ビューでrenderを使うのは初めてですね。

ビューでのレンダーは部分テンプレートの表示に使います。

 

実際に部分テンプレートを作ったほうが分かりやすいと思うので、さっそく作ってみましょう。

app/views/layoutsフォルダに、_header.html.erbファイルを作ってください。

_header.html.erbに下記のコードを追加してください。

<header>
  <div class="header-logo">
    Rails beginner
  </div>
  <ul>
    <li>
      <%= link_to("ログアウト", "/users/sign_out", method: :delete, data: {confirm: "本当にログアウトしますか?"}) %>
    </li>
  </ul>
</header>

このコードはapp/views/layouts/application.html.erbの<%= render ‘layouts/header’ %>部分で表示されます。

 

つまり部分テンプレートを使えば、別ファイルにコードを切り分けて見やすくできます。

あとはよく使うビューのコードを切り分けて、繰り返し同じコードを書くことも防げます。

 

あと_header.html.erbの7行目に見慣れないコードがありますね。

<%= link_to("ログアウト", "/users/sign_out", method: :delete, data: {confirm: "本当にログアウトしますか?"}) %>

railsはリンクをlink_toメソッドを使って作成できます。

まず第1引数には画面に表示させたい文字を書きます。

今回は”ログアウト”ですね。

次の第2引数には遷移してほしいURLを書きます。

deviseはログアウトのリンクを自動で生成してくれています。

それが “/users/sign_out” です。

その後のmetod: :deleteは、HTTPリクエストメソッドのgetメソッドやpostメソッドを指定します。

自動で作成される”/users/sign_out”のHTTPリクエストメソッドがdeleteなので、今回はdeleteメソッドを指定しています。

最後の data: {confirm: “本当にログアウトしますか?”} で、リンクがクリックされたときにアラートを表示することができます。

 

最後にcssを書いてスタイルを整えます。

/* コメントがたくさん */

.container {
  width: 80%;
  margin: 20px auto;
}

input, textarea {
  box-sizing: border-box;
  width: 400px;
  padding: 6px;
}

input[type="submit"] {
  background-color: #FF5D57;
  border-radius: 5px;
  color: white;
  padding: 10px;
  text-align: center;
  width: 150px;
}

textarea {
  height: 120px;
}

ul {
  margin: 0px;
}

li {
  list-style: none;
  float: left;
  padding-right: 10px;
}

header {
  padding: 15px;
  background-color: #FF5D57;
  color: #fff;
  height: 20px;
}

header a {
  color: #fff;
}

.header-logo {
  float: left;
  padding-right: 20px;
}

それでは実際にログアウトボタンをクリックしてみましょう。

クリックするとログアウトが成功したというメッセージが表示されますね。

railsビギナー

これで新規登録・ログイン・ログアウトができるようになりました。

 

少し使いやすくするために、ログインしている状態ではログアウトリンクを表示して、ログインしていない状態では新規登録とログインのリンクを表示します。

あと左上にあるRails beginnerをクリックしたときはトップページに戻り、ログイン時には質問の新規作成フォームへのリンクもつけてあげます。

app/views/layouts/_header.html.erbのコードを、下記のように書き換えてください。

<header>
  <div class="header-logo">
    <%= link_to("Rails beginner", "/") %>
  </div>
  <ul>
    <% if user_signed_in? %>
      <li>
        <%= link_to("質問作成", "/questions/new") %>
      </li>
      <li>
        <%= link_to("ログアウト", "/users/sign_out", method: :delete, data: {confirm: "本当にログアウトしますか?"}) %>
      </li>
    <% else %>
      <li>
        <%= link_to("新規登録", "/users/sign_up") %>
      </li>
      <li>
        <%= link_to("ログイン", "/users/sign_in") %>
      </li>
    <% end %>
  </ul>
</header>

<% if user_signed_in? %>に書かれているuser_signed_in?は、deviseが用意してくれているヘルパーメソッドで、ユーザーがログインしているか判定します。

 

ちなみにRubyのコードを画面に表示したいときは、<%= %>で囲む必要がありますが、表示させる必要がない場合は<% %>で囲めばOKです。

 

あとヘッダーにある「Rails beginner」には下線を引かないようにします。

ログインフォームデザインも、少し整えてあげました。

/* コメントがたくさん */

.container {
  width: 80%;
  margin: 20px auto;
}

input, textarea {
  box-sizing: border-box;
  width: 400px;
  padding: 6px;
}

input[type="checkbox"] {
  width: 15px;
}

input[type="submit"] {
  background-color: #FF5D57;
  border-radius: 5px;
  color: white;
  padding: 10px;
  text-align: center;
  width: 150px;
}

textarea {
  height: 120px;
}

ul {
  margin: 0px;
}

li {
  list-style: none;
  float: left;
  padding-right: 10px;
}

header {
  padding: 15px;
  background-color: #FF5D57;
  color: #fff;
  height: 20px;
}

header a {
  color: #fff;
}

.header-logo {
  float: left;
  padding-right: 20px;
}

.header-logo a {
  text-decoration: none;
}

 

実際にログインしている場合と、ログインしていない場合の画面表示を見てみましょう。

先ほど作ったアカウントで、ログインしたり、ログアウトしたりしてください。

(ログイン画面へのURLは「http://localhost:3000/users/sign_in」です)

 

ログインしている場合↓

railsビギナー

ログインしていない場合↓

railsビギナー

ログインしている場合と、そうでない場合とで、ヘッダーのリンクの表示が切り替わりますね。

メール認証機能をつけよう

rails beginner

今のままでは自分以外のメールアドレスでも登録できてしまいます。

本人のメールアドレスでのみ登録を受け付けたいですよね?

そこでメール認証の機能を付け加えます。

 

deviseでメール認証の仕組みを実現するのは簡単です。

app/models/user.rbに、:confirmableを追加してください。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable
end

次にconfig/environments/development.rbを開いてください。

33行目あたりに下記のコードを追加しましょう。

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

 

これで新規登録の際、メール認証が加わりました。

 

次に開発環境で、送られたメールを確認するために、letter_opener_webというgemを入れます。

Gemfileを開いて、group :development do 〜 endの中に、下記コードを追加。

gem 'letter_opener_web'

こんな感じ

railsビギナー

 

ターミナルで下記コマンドを実行。

bundle install

 

config/environments/development.rbに下記コードを追加しましょう。

書く場所は上記で書いた config.action_mailer.default_url_options = { host: ‘localhost’, port: 3000 } の下あたりでOK!

config.action_mailer.delivery_method = :letter_opener_web

 

config/routes.rbに下記コードを追加

Rails.application.routes.draw do
  devise_for :users

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  get "/questions", to: "questions#index"
  get "/questions/new", to: "questions#new"
  post "/questions", to: "questions#create"
  get "/questions/:id", to: "questions#show"
end

これで送信されたメールを確認できるようになりました!

それでは試しにサーバを再起動してから、新しいユーザーを登録してみましょう。

(再起動の方法はいつも通りです。サーバを起動させたターミナルでcontrol + cをクリック。その後 rails sを入力)

 

ブラウザを開いて、「http://localhost:3000/users/sign_up」にアクセス!

railsビギナー

 

成功したら「A message with a confirmation link has been sent to your email address. Please follow the link to activate your account.」というメッセージが表示されます。

次は送信されたメールを確認します。

「localhost:3000/letter_opener」にアクセス。

メールが確認できます。

railsビギナー

メールの中にあるリンクをクリック。

ログインに画面に遷移して、メールアドレスが確認できたというメッセージが表示されています。

railsビギナー

先ほど登録したメールアドレスとパスワードを入力すればログインできます!

以上でメール認証を使った新規登録ができるようになったと確認できました。

 

メールの文章内容や、新規登録・ログインの画面の文章を編集したい場合は、deviseが用意しているコマンドでビューを作成して編集しましょう。

rails g devise:views

app/views/deviseの中に様々なフォルダがあります。

まずはapp/views/devise/registrations/new.html.erbを、少し編集してみましょう。

このファイルは、ユーザー新規登録のフォーム画面に該当します。

<h2>新規登録</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> 文字以上)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "登録" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

ログアウトしてから「localhost:3000/users/sign_up」にアクセスすると、編集した部分が変更されていることが分かりますね。

このようにdeviseで作られたビューも編集することができます。

ユーザーの詳細ページを作成

rails beginner

登録されたユーザーの情報を表示できるようにします。

とはいえ今のままでは、メールアドレスを登録してもらっただけなので、名前やプロフィールなどの表示させたいユーザー情報がありません。

そこでusersテーブルに、とりあえずnameカラムだけでも追加しましょう。

テーブルにカラムを追加するには、まずマイグレーションファイルを作成します。

下記のコマンドをターミナルに入力してください。

rails g migration AddNameToUsers

作成されたマイグレーションファイルを下記のように書き換えます。

class AddNameToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :name, :string
  end
end

add_columの後は、テーブル名・カラム名・型の順番で指定します。

 

それではターミナルにrails db:migrateを入力して、マイグレーションファイルを反映させましょう。

rails db:migrate

 

とはいえカラムがあるだけでは意味がありません。

nameを保存できるように、新規登録のフォームを書き換えます。

app/views/devise/registrations/new.html.erbを少し書き換えます。

<h2>新規登録</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> 文字以上)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "登録" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

これでnameを保存するための準備は整ったように見えますが…

じつは問題点があります。

それはストロングパラメータです。

ストロングパラメータって何だっけ?という方は、第1章を読み返しましょう。

deviseはストロングパラメータの設定で、保存できるカラムが決まっています。

 

nameカラムも保存できるように設定したいなら、ユーザーの新規登録機能を担っているコントローラーに、nameカラムも許可するというコードを書けばいいのですが…

ユーザーの新規登録機能を担っているコントローラーのファイルってどれでしょうか?

じつはまだ生成していないので編集できません。

そこで下記のコマンドをターミナルに入力してファイルを生成して、編集できるようにします。

rails g devise:controllers users

app/controllers/users/registrations_controller.rbを見てください。

このファイルが新規登録に関するコントローラーです。

数カ所書き換えるだけで、nameカラムを登録できるようになります。

# frozen_string_literal: true

class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  # def new
  #   super
  # end

  # POST /resource
  # def create
  #   super
  # end

  # GET /resource/edit
  # def edit
  #   super
  # end

  # PUT /resource
  # def update
  #   super
  # end

  # DELETE /resource
  # def destroy
  #   super
  # end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  # def cancel
  #   super
  # end

  protected

  # If you have extra params to permit, append them to the sanitizer.
  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_account_update_params
  #   devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  # end

  # The path used after sign up.
  # def after_sign_up_path_for(resource)
  #   super(resource)
  # end

  # The path used after sign up for inactive accounts.
  # def after_inactive_sign_up_path_for(resource)
  #   super(resource)
  # end
end

カスタマイズしたコントローラーファイルを適用するために、ルーティングファイルを下記のように書き換えます。

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  get "/questions", to: "questions#index"
  get "/questions/new", to: "questions#new"
  post "/questions", to: "questions#create"
  get "/questions/:id", to: "questions#show"
end

一度、サーバを再起動させましょう。

再起動した後に「http://localhost:3000/users/sign_up」にアクセスしてください。

適当な名前・メールアドレス・パスワードを入力して登録しましょう。

railsビギナー

登録ボタンをクリックした後は、上記でも行った通りメールを確認しましょう。

「localhost:3000/letter_opener」にアクセスして、リンクをクリックしてください。

これで名前を持ったユーザーが登録できました。

 

それでは次は、ターミナルに下記のコマンドを入力して、ユーザーの詳細ページを表示するためのコントローラーを作成してください。

rails g controller users

app/controllers/users_controller.rbに、showアクションを追加しましょう。

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

おっとルーティングの記述を忘れていましたね。

config/routes.rbを開いて、下記のコードを追加してください。

(userに関するコードは上にまとめました)

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

  get "/users/:id", to: "users#show"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  get "/questions", to: "questions#index"
  get "/questions/new", to: "questions#new"
  post'/questions', to: "questions#create"
  get '/questions/:id', to: 'questions#show'
end

ビューファイルも作成しましょう。

app/views/usersフォルダに、show.html.erbファイルを作成してください。

作成したshow.html,erbに下記のコードを追加します。

<h2>ユーザーの詳細ページ</h2>
<%= @user.name %>

先ほど作成したユーザーの詳細ページを見てみます。

「http://localhost:3000/users/3」にアクセスしてください。

railsビギナー

ユーザーの名前がしっかり表示されていますね!

ユーザーと質問を紐づける

プログラミング

ユーザーの新規登録・ログイン機能は作れました。

次はquestionsテーブルに、user_idというカラムを持たせて、誰がその質問を作ったのか分かるようにします。

 

まずはマイグレーションファイルを作りましょう。

rails g migration AddUserIdToQuestions

作成されたマイグレーションファイルに下記のコードを追加します。

class AddUserIdToQuestions < ActiveRecord::Migration[6.0]
  def change
    add_reference :questions, :user, foreign_key: true
  end
end

user_idカラムなど、テーブルの関連付けに必要なカラムは、referenceで作ると思ってください。

:questionsで、どのテーブルにカラムを追加するかを決めています。

 

次にある:userがカラム名です。

add_referenceを使うと、自動的にカラム名に_idがつけられます。

そのため:userと書くだけでOKです。

最後のforeign_key: trueは外部キー制約と呼ばれるもので、今の段階では、外部キーを作るときは、これがあるとよりセキュリティが強固になるという知識で十分です。

 

それではターミナルに下記コマンドを入力してください。

rails db:migrate

 

とりあえずquestionsテーブルに、user_idというカラムは作れました。

しかし質問を新規作成したときに、user_idに値を入れる処理を何も書いていません。

そこでquestionsコントローラーのcreateアクションを少し書き換えます。

class QuestionsController < ApplicationController
  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/new")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end
end

current_userというのは、deviseが用意してくれているヘルパーメソッドで、今現在ログインしているユーザーを指しています。

 

つまり @question.user_id = current_user.idで、user_idカラムに今現在ログインしているユーザーのIDを入れてから、次の@question.saveでテーブルに保存するという流れです。

 

これで質問の作成者が分かるようになりました。

 

次はアソシエーション(モデルの関連付け)について解説します。

モデルを関連付けすると、とあるユーザーAが作成した質問の取得を簡潔なコードで実装できたり、ユーザーAがアカウントを消した時にユーザーAが作成した質問も一緒に削除できたりします。

 

今回の場合、1人のユーザーは多くの質問を作成できます。

逆に1つの質問の作者は絶対に1人しかいません。

これはユーザーと質問が、1対多の関係になっていると言えます。

その場合、アソシエーションは下記のように記述します。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  has_many :questions
end
class Question < ApplicationRecord
  belongs_to :user

  validates :title, presence: true, length: { maximum: 100 }
  validates :body, length: { maximum: 3000 }
end

1人のユーザーは多くの質問を持つから、has_many :questionsと書きます。

逆に1つの質問は1人のユーザーに属するので、belongs_to :userと書きます。

 

それではユーザー詳細ページに、そのユーザーが作成した質問のタイトル一覧を表示してみます。

もしアソシエーションを使わずに書くなら、このようなコードになります。

まずapp/controllers/users_controller.rbに下記のコードを書きます。

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    @questions = Question.where(user_id: @user.id)
  end
end

※whereメソッドが何かピンッとこない人はプロゲートを復習!

次にapp/views/users/show.html.erbに下記コードを書きます。

<h2>ユーザーの詳細ページ</h2>
<%= @user.name %>
<hr>
<% @questions.each do |question| %>
  <%= link_to(question.title, "/questions/#{question.id}") %>
<% end %>

※each doを使った繰り返しの処理がピンッとこない人もプロゲートを復習!

 

アソシエーションを使うと、questionsコントローラーには何も書き足す必要がありません。

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

@user.questionsと書けば、そのユーザーが作成した質問を取得できます。

<h2>ユーザーの詳細ページ</h2>
<%= @user.name %>
<hr>
<% @user.questions.each do |question| %>
  <%= link_to(question.title, "/questions/#{question.id}") %>
<% end %>

それではidが3のユーザー(教材通りに進めていたら、emailがemail3@exampleで、名前が佐藤 太郎のユーザー)でログインして、質問を新規作成してみましょう。

railsビギナー

作成後、「http://localhost:3000/users/3」にアクセスしてください。

idが3のユーザーが作成した質問のタイトルが表示されていますね。

さらに質問を作成すれば、どんどん質問のタイトルが増えていきます。

 

次はユーザーがアカウントを削除したとき、そのユーザーが作成した質問も一緒に消えるようにする処理を書きます。

下記のコードを書けばOKです。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  has_many :questions, dependent: :destroy
end

簡単ですね。

これでユーザーがアカウントを削除したとき、そのユーザーが作成した質問も一緒に削除されます。

アソシエーションはとにかく便利なので、使いこなせるようになりましょう!

質問の編集機能と削除機能を作る

rails beginner

今までは質問の作成者が誰か分からず、質問の作成者のみに、その質問を編集・削除できるようにする機能は作れませんでした。

しかし今なら質問の作成者だけ、編集・削除できるように制限もかけられます。

 

それではまずルーティングを作成しましょう。

config/routes.rbに下記のコードを追加してください。

Rails.application.routes.draw do
  devise_for :users
  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end
  get "/users/:id", to: "users#show"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  get "/questions", to: "questions#index"
  get "/questions/new", to: "questions#new"
  post'/questions', to: "questions#create"
  get '/questions/:id', to: 'questions#show'
  get "/questions/:id/edit", to: "questions#edit"
  patch "/questions/:id", to: "questions#update"
  delete "/questions/:id", to: "questions#destroy"
end

createアクションのときは、postメソッドを使いましたね。

updateアクションのときは、postメソッドではなくpatchメソッドを使います。

destroyアクションのときは、deleteメソッドを使います。

 

ここで1つ、脱・初心者テクニックをご紹介。

index、show、new、create、edit、update、destroy…

これらのアクションに対応するルーティングを全て書くのは一苦労ですよね?

そこでRailsはルーティングを楽に書く仕組みを用意してくれています。

今まで書いたquestionsコントローラーに関するルーティングは、一行でまとめられます。

config/routes.rbを、下記のように書き換えてください。

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

  get "/users/:id", to: "users#show"
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  root to: 'questions#index'
  resources :questions
end

これだけです。

resources :questionsと書くだけで、index、show、new、create、edit、update、destroyアクションに対応するルーティングを、全て書いたことになります。

 

けれどもこれだと実際にどのようなURLになっているか?

どのURLが、どのコントローラーの、どのアクションに対応しているかが分かりません。

そこでRailsはURLを確認するためのコマンドを用意しています。

ターミナルに下記のコマンドを入力しましょう。

rails routes

なんだかよく分からない記述がたくさん出てきましたね。

railsビギナー

すこし説明します。

questionsのURLがある箇所に記述に注目。

右端がコントローラーとアクションを示しています。

railsビギナー

真ん中あたりにHTTPリクエストメソッドとURLが書かれています。

railsビギナー

つまりquestionsコントローラーのindexアクションに対応するURLは/questionsで、HTTPリクエストメソッドはGETだと明記しているのです。

※(.:format)は今のところは無視して大丈夫です。

 

次はquestionsコントローラーに、editアクションとupdateアクションを作りましょう。

class QuestionsController < ApplicationController
  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end
end

違う部分もありますが、新規作成と流れが似ていますね。

editアクション(newアクション)が、フォーム画面を作ります。

updateアクション(createアクション)が、データベースに保存する処理を書きます。

 

それではeditアクションに対応する、edit.html.erbを作りましょう。

<%= form_with(model: @question, local: true) do |f| %>
  <div>
    <%= f.label :title, "タイトル" %><br>
    <%= f.text_field :title %>
  </div>
  <div>
    <%= f.label :body, "本文" %><br>
    <%= f.text_area :body %>
  </div>
  <%= f.submit %>
<% end %>

form_with(model: の横にある@questionですが、newアクションのときは空のインスタンスが入っていましたね。

しかし今回は空のインスタンスではありません。

questionsコントローラーのeditアクションを見てみましょう。

class QuestionsController < ApplicationController
  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end
end

データベースから取得した質問が@questionに入っていますね。

つまりtitleやbodyにしっかり値が入っています。

そのためform_withは、「これは新規作成フォームではなく編集フォームを作ればいいんだな」と判断してくれます。

submitボタンを押せば、updateアクションが走ります。

 

updateアクションの処理を確認しましょう。

class QuestionsController < ApplicationController
  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end
end

@question = Question.find(params[:id])で、編集前の質問を取得しています。

次の@question.update(question_params)で、フォームに入力された値を使って更新します。

成功した場合の処理と、失敗した場合の処理は、新規作成と同じですね。

これでひとまず編集機能ができました。

 

質問の詳細ページに編集ページへのリンクを貼ってください。

<p><%= @question.title %></p>
<p><%= @question.body %></p>
<%= link_to("編集", "/questions/#{@question.id}/edit") %>

一度、ブラウザを開いて「localhost:3000/questions/2」にアクセスしてみましょう。

railsビギナー

編集リンクがあるのでクリックしてください。

編集フォームが出てきましたね。

railsビギナー

入力されている文字を何か適当に変えて、submitボタンをクリックしましょう。

しっかり編集されましたね。

railsビギナー

 

次は削除機能を作ります。

questionsコントローラーに、destroyアクションを作ります。

class QuestionsController < ApplicationController
  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/new")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  def destroy
    @question = Question.find(params[:id])
    @question.destroy
    flash[:notice] = "成功!"
    redirect_to("/questions")
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end
end

@question = Question.find(params[:id])で、削除したい質問を取得しています。

@question.destroyで質問を削除。

削除したあとは、質問一覧ページに遷移させます。

これでひとまず削除機能ができました。

 

最後に削除リンクを、質問の詳細ページに貼ります。

<p><%= @question.title %></p>
<p><%= @question.body %></p>
<%= link_to("編集", "/questions/#{@question.id}/edit") %>
<%= link_to("削除", "/questions/#{@question.id}", method: :delete, data: {confirm: "本当に削除しますか?"}) %>

実際にリンクをクリックして削除してみましょう。

ブラウザを開いて「http://localhost:3000/questions/1」にアクセスしてください。

削除リンクをクリックすると、無事に削除できましたね!

railsビギナー

編集機能も削除機能もできましたが、まだ問題がいくつかあります。

まずログインしているユーザーのみ、質問を編集・削除できるようにします。

ついでに質問作成もログインしているユーザーのみ可能にします。

deviseのメソッドを使えば、簡単にログインしていないユーザーの作成・編集・削除を防げます。

questionsコントローラーにbefore_actionを書きます。

class QuestionsController < ApplicationController
  before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy]

  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/new")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  def destroy
    @question = Question.find(params[:id])
    @question.destroy
    flash[:notice] = "成功!"
    redirect_to("/questions")
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end
end

before_actionを記述すると、newやcreate、editといったアクションを実行する前に、何かしらの処理を実行できます。

今回はdeviseが用意してくれたメソッドの「authenticate_user!」を実行。

authenticate_user!を使えば、ログインしているユーザーのみアクセスを許可できます。

しかし質問の詳細ページなど、ログインしていないユーザーにも見てもらいたいページもあると思います。

そこでonly: [:new, :create, :edit, :update, :destroy]と記述して、new、create、edit、update、destroyアクションのみ、authenticate_user!を適用させています。

 

次は質問を作成した本人のみ、その質問を編集・削除できるようにします。

こちらもbefore_actionを使います。

class QuestionsController < ApplicationController
  before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy]
  before_action :ensure_correct_user, only: [:edit, :update, :destroy]

  def index
    @test = "テストテキスト"
  end

  def show
    @question = Question.find(params[:id])
  end

  def new
    @question = Question.new
  end

  def create
    @question = Question.new(question_params)
    @question.user_id = current_user.id
    if @question.save
      flash[:notice] = "成功!"
      redirect_to("/questions/new")
    else
      flash.now[:alert] = "失敗!"
      render("questions/new")
    end
  end

  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(question_params)
      flash[:notice] = "成功!"
      redirect_to("/questions/#{@question.id}")
    else
      flash.now[:alert] = "失敗!"
      render("questions/edit")
    end
  end

  def destroy
    @question = Question.find(params[:id])
    @question.destroy
    flash[:notice] = "成功!"
    redirect_to("/questions")
  end

  private
    def question_params
      params.require(:question).permit(:title, :body)
    end

    def ensure_correct_user
      @question = Question.find_by(id: params[:id])
      if @question.user_id != current_user.id
        flash[:alert] = "権限がありません"
        redirect_to("/questions/#{@question.id}")
      end
    end
end

authenticate_user!はdeviseが用意してくれたメソッドですが、ensure_correct_userは自分で作る必要があります。

そこでprivateの下部にコードを書いています。

ensure_correct_userの処理の中身を少し解説します。

@question = Question.find_by(id: params[:id])で、質問をデータベースから取得します。

※find_byメソッドの使い方が分からない方はプロゲートを復習!

 

if @question.user_id != current_user.idで、質問のuser_idと、現在ログインしているユーザーのidを比べています。

違う場合、redirect_to(“/questions/#{@question.id}”)で、質問の詳細ページにとばします。

 

まとめるとedit、update、destroyアクションを実行する前に、ensure_correct_userが実行されて、本当に質問を作成した本人か確認し、違うかった場合は質問の詳細ページにとばすという処理を書きました。

 

一度、ブラウザを開いてidが2のユーザーでログインした後に、「localhost:3000/questions/2」にアクセスして、編集リンクをクリックしてみましょう。

(教材通りに進めている場合、idが2の質問は、idが3のユーザーが作成しています。そのためidが2のユーザーには編集権限がありません)

 

idが2のユーザーでログイン↓

railsビギナー

「localhost:3000/questions/2」にアクセスして、編集リンクをクリックすると…

railsビギナー

リダイレクトされて質問の詳細ページに戻りましたね。

 

最後に質問の編集・削除リンクを、質問を作成した本人にのみ表示します。

今のままだと誰にでも、編集リンク・削除リンクが表示されます。

これを質問の作成者にのみ、編集リンク・削除リンクを表示させます。

app/views/questions/show.html.erbのコードを書き換えます。

<p><%= @question.title %></p>
<p><%= @question.body %></p>
<% if @question.user_id == current_user.id %>
  <%= link_to("編集", "/questions/#{@question.id}/edit") %>
  <%= link_to("削除", "/questions/#{@question.id}", method: :delete, data: {confirm: "本当に削除しますか?"}) %>
<% end %>

実際に質問作成者以外のアカウントでログインして、質問詳細ページを見てみましょう。

今回はidが2のユーザーでログインしている状態で、「localhost:3000/questions/2」にアクセスします。

railsビギナー

しっかり非表示になっていますね!

Railsビギナー 第2章「ユーザー登録・ログイン機能作成」まとめ

rails beginner

お疲れ様でした!

第2章終了です。

第3章では質問への回答機能を作成します。

第3章を終えれば、質問サイトっぽい感じが出てきます。

「これ作れる自分ってだいぶ成長したのでは?」と思えること間違いなしです!

頑張っていきましょう!

←Railsビギナー 第1章はこちら

Railsビギナー 第3章はこちら→