Railsチュートリアル 3章 静的なページ

Railsチュートリアル3章をやって大事だと思ったことメモしていきます!

尚、テストコードはRspecを利用して書きました。

 

静的ページのルーティング、コントローラー

 

ルーティング

Rails.application.routes.draw do
root 'static_pages#home'
get 'static_pages/home'
get 'static_pages/help'
get 'static_pages/about'
get 'static_pages/contact'
resources :users

end

getの4つが静的なページのルーティング。

root_pathは#であるところがちがう。

コントローラー名/アクション名って感じだね。

 

コントローラー

class StaticPagesController < ApplicationController
def home
end

def help
end

def about
end

def contact
end
end

 

provideとyeild

application.html.erb

<!DOCTYPE html>
<html>
<head>
<title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</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>
<%= yield %>
</body>
</html>

 

home.html.erb

<% provide(:title, "Home") %>
  ここ!!
<h1>Sample App</h1>
<p>
This is the home page for the
sample application.
</p>

 

provideで設定したやつをyeildで呼び出してるイメージ。

そうすることでprovide内で書いたことをyeildで埋め込んでいる。

 

テストコード

static_pages_spec.rb

require 'rails_helper'

RSpec.describe "StaticPages", type: :system do
 
def setup
@base_title = "Ruby on Rails Tutorial Sample App"
end

it 'should get root' do
visit root_path
expect(current_path).to eq root_path
ページを訪れたときにちゃんとそのページに移れてるかをテスト
expect(page).to have_title "Home | #{@base_title}"
タイトルをテストするときはhave_titleを使う。これはhave_contentと使い方一緒である。
#{@base_title}としているのは同じコードが繰り返されているから上のsetupでまとめている。式を利用して文字列に
入れてあげてる。

end

it 'should get home' do
visit static_pages_home_path
expect(current_path).to eq (static_pages_home_path)
expect(page).to have_title "Home | #{@base_title}"

end

it 'should get help' do
visit static_pages_help_path
expect(current_path).to eq (static_pages_help_path)
expect(page).to have_title "Help | #{@base_title}"

end

it 'should get about' do
visit static_pages_about_path
expect(current_path).to eq (static_pages_about_path)
expect(page).to have_title "About | #{@base_title}"

end

it 'should get contact' do
visit static_pages_contact_path
expect(current_path).to eq (static_pages_contact_path)
expect(page).to have_title "Contact | #{@base_title}"

end


end

 

system specではgetやpost----pathやrender_template等は使えないと思われる。

visit pathおk

urlもだめ。path◯

 

3章は以上です!

Railsチュートリアル14章 フォローとフォロワー

14章 フォローとフォロワーの関係がややこしいのでまとめる

 

ユーザーモデル内

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
has_many :following, through: :active_relationships, source: :followed
 active_relationshipsのforeignキーはfollower_idで逆じゃないのか?ってなったがこれで正しい。
これは片方(逆の関係であるもの)を固定することによって片方の情報を持ってきている。
図を見た方がわかりやすい(14.7)
active
followed | folower
1 2
2 1
3 1
4 1
foreignキーでfollewer_idを指定する(今回であれば1)によってフォローしているfollowedの情報を取ってくる。
has_many :followers, through: :passive_relationships, source: :follower
上と考え方一緒。逆の関係
図(14.9)
. . . end

Rails チュートリアル13章

今回は13章をやっていく中で必要だと思ったところをメモしていきます。

 

・複合キーインデックス

micropostのマイグレーションファイル

 
 
class CreateMicroposts < ActiveRecord::Migration[6.0]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end
複数のキーをセットで扱うことができる。
こうすることでuser_idに紐付けられたmicropostsを作成順(今回は作成の逆順にする)で取り出す
 
・default scope
 
 Micropostモデル
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
 
このようにして作成の逆順で取り出すよう設定している。
 
 
・time_ago_in_wordsメソッド
 
app/views/microposts/_micropost.html.erbapp/views/microposts/_micropost.html.erb
 
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>
何分前とかって出るようにする。例えば3分前、3時間前、3日前。
 
 
マクロポストの作成(build)
 
root_pathのhomeでuserに紐ずくmicropostを作成する。
何かに紐付いたものを生成する場合、newではなくbuildになる。
作成はhomeで保存はmicropostのcreate
 
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    @micropost = current_user.microposts.build if logged_in?
buildになっている
end def help end def about end def contact end end
 
app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
      This is the home page for the
      <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
      sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
  </div>

  <%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"),
              "https://rubyonrails.org/" %>
<% end %>
 
app/views/shared/_user_info.html.erb 
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>
 
app/views/shared/_micropost_form.html.erb
<%= form_with(model: @micropost, local: true) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
 
app/controllers/microposts_controller.rb
 
class MicropostsController < ApplicationController
before_action :logged_in_user, only: [:create, :destroy]
before_action :correct_user, only: :destroy

def create
@micropost = current_user.microposts.build(micropost_params)
buildを使ってる
@micropost.image.attach(params[:micropost][:image])
if @micropost.save
flash[:success] = "Micropost created!"
redirect_to root_url
else
@feed_items = current_user.feed.paginate(page: params[:page])
render 'static_pages/home'
end
end
 
feed
 
class User < ApplicationRecord
  .
  .
  .
  # 試作feedの定義
  # 完全な実装は次章の「ユーザーをフォローする」を参照
  def feed
    Micropost.where("user_id = ?", id)
  end

    private
    .
    .
    .
end
 ?を使うことでidをエスケープしている
SQL文に変数を代入する場合は常にエスケープする必要がある。
 
app/controllers/static_pages_controller.rb
 
class StaticPagesController < ApplicationController

  def home
    if logged_in?
      @micropost  = current_user.microposts.build
このmicropostは先ほど書いたやつ(build)なので関係ない
@feed_items = current_user.feed.paginate(page: params[:page]) end end def help end def about end def contact end end
app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

 

app/views/shared/_feed.html.erb

<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

 

<%= render @feed_items %>

@feed_itemsはMicropostsのクラスを持っているため、micropostsのパーシャルを呼び出すことができた。

app/views/microposts/_micropost.html.erbを呼び出してる

 

microposts作成失敗の場合に備えて

app/controllers/microposts_controller.rb

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = current_user.feed.paginate(page: params[:page])
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

 

ページネートでエラーが出るので解決。

micropostsをcreateするcreateアクションがMicropostsコントローラーにあるので、Micropostsコントローラーでページネートしようとしている。

そこで、コントローラーとアクションを指定してあげることで解決する。

app/views/shared/_feed.html.erb

<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items,
                    params: { controller: :static_pages, action: :home } %>
<% end %>

Railsチュートリアル12章 パスワード再設定

Password Reset

ログインする際、パスワードを忘れてしまった場合にパスワード再設定をする。

userのedit,updateでパスワードを変えるのは違う。それはパスワードが分かっていててログインできてる状態。

今回はパスワードが分からなくて、ログインできない状態

 

全体像

ログインページからpassword_fogetをクリックしてパスワード再設定ページへ。

emailを入力すれば(new),createアクションでreset_token,reset_digestを生成。

reset_digestをカラムに入れる。またこのcreate アクションでメールを送る。

メール内に書かれたedit_pathをクリック。edit_pathでpassword とpassword_confirmationを入力(edit)するとupdateアクションへ。passwordとpassword_confirmationをupdateしてあげる。

変更されたpasswordでログインできるようになる。

-----------------------------------------------------------------------------------------------------------------

 

password_resetコントローラー作成

$ rails generate controller PasswordResets new edit --no-test-framework

 

ルーティング追加

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'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
end

 

ログインページにパスワード再設定ページへのリンクを追加

app/views/sessions/new.html.erb

 

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= link_to "(forgot password)", new_password_reset_path %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

 

マイグレーション

$ rails generate migration add_reset_to_users reset_digest:string \
> reset_sent_at:datetime

 

$ rails db:migrate

 

パスワード再設定ページ

app/views/password_resets/new.html.erb

<% provide(:title, "Forgot password") %>
<h1>Forgot password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: password_resets_path, scope: :password_reset,
                    local: true) do |f| %>
urlとscopeを書けば、モデルでなくても大丈夫 <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.submit "Submit", class: "btn btn-primary" %> <% end %> </div> </div>

 

コントローラー createアクション

app/controllers/password_resets_controller.rb

class PasswordResetsController < ApplicationController

  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

  def edit
  end
end

 

app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  before_save   :downcase_email
  before_create :create_activation_digest
  .
  .
  .
  # アカウントを有効にする
  def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end

  # 有効化用のメールを送信する
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  # パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  private

    # メールアドレスをすべて小文字にする
    def downcase_email
      self.email = email.downcase
    end

    # 有効化トークンとダイジェストを作成および代入する
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

 

メール

app/mailers/user_mailer.rb

class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset(user)
    @user = user
    mail to: user.email, subject: "Password reset"
  end
end

 

app/views/user_mailer/password_reset.text.erb

To reset your password click the link below:

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

This link will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password will stay as it is.

 

 

app/views/user_mailer/password_reset.html.erb

<h1>Password reset</h1> <p>To reset your password click the link below:</p> <%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email) %> <p>This link will expire in two hours.</p> <p> If you did not request your password to be reset, please ignore this email and your password will stay as it is. </p>

 

プレビュー

test/mailers/previews/user_mailer_preview.rb

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at
  # http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    user = User.first
    user.reset_token = User.new_token
    UserMailer.password_reset(user)
  end
end

サーバーログにメールが送られて来ている。

 

editのビュー

app/views/password_resets/edit.html.erb

<% provide(:title, 'Reset password') %>
<h1>Reset password</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, url: password_reset_path(params[:id]),
                  local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= hidden_field_tag :email, @user.email %>
updateアクションでもemailからreset_tokenを持ってくる必要があるため。
f.hidden_fieldにしてしまうとparams[:user][:email]になってしまう。
hidden_fieldだとparam[:email]
<%= 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 "Update password", class: "btn btn-primary" %> <% end %> </div> </div>

コントローラー edit
app/controllers/password_resets_controller.rb
 
class PasswordResetsController < ApplicationController
  before_action :get_user,   only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
  .
  .
  .
  def edit
  end

  private

    def get_user
      @user = User.find_by(email: params[:email])
    end

    # 正しいユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end
end

 

コントローラー update

app/controllers/password_resets_controller.rb

class PasswordResetsController < ApplicationController
  before_action :get_user,         only: [:edit, :update]
  before_action :valid_user,       only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update]    # (1)への対応

  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
  end

  def edit
  end

  def update
    if params[:user][:password].empty?                  # (3)への対応
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update(user_params)                     # (4)への対応
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'                                     # (2)への対応
    end
  end

  private

    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end

    # beforeフィルタ

    def get_user
      @user = User.find_by(email: params[:email])
    end

    # 有効なユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end

    # トークンが期限切れかどうか確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
end

 

app/models/user.rb

class User < ApplicationRecord
  .
  .
  .
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

2時間前より早い=2時間を過ぎていると解釈する private . . . end

 

app/controllers/password_resets_controller.rb

 

class PasswordResetsController < ApplicationController
  .
  .
  .
  def update
    if params[:user][:password].empty?
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update(user_params)
      log_in @user
      @user.update_attribute(:reset_digest, nil)
     2時間の間に不正アクセスされないようにするため。
flash[:success] = "Password has been reset." redirect_to @user else render 'edit' end end . . . end

 

 

Railsチュートリアル11章 アカウントの有効化

アカウントの有効化

ユーザーを新規登録する際にメールを送って、メール内に貼ってあるリンクをクリックすることで、初めてログインできるようになる。

 

全体の流れ

ユーザーがsave出来たら、before_createでactivation_tokenを作り、それをdigest化してハッシュにしたactivation_digestを作る(self.activation_digestとなっているのでuserのカラム内に入る)。そしてuserコントローラー内でメールを送る。この段階でuserはcreateされている。このメール内のedit_pathに行くとauthenticatedで、先ほどdigest化したやつとactivation_tokenが合っているか確かめる。合っていたらactivationコントローラー内のeditアクションでactivatedをtrueにする。セッションコントローラーでactivatedがtrueであればログインできる。falseであればログインできないように記述する。これによって初めてログインできるようになる。

--------------------------------------------------------------------------------------------------------------------

まずはacctount_activationコントローラー作る

$ rails generate controller AccountActivations




ルーティング
edit設定
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'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  resources :account_activations, only: [:edit]
end



マイグレーション
activation_digestカラム等作る
$ rails generate migration add_activation_to_users \
> activation_digest:string activated:boolean activated_at:datetime

class AddActivationToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
    add_column :users, :activated_at, :datetime
  end
end


$ rails db:migrate


モデル内にactivation_token,activation_digestを作る記述をかく。
app/models/user.rb
*before_saveのところは追加しているのではなく変更しているだけ。
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  .
  .
  .
  private

    # メールアドレスをすべて小文字にする
    def downcase_email
      self.email = email.downcase
    end

    # 有効化トークンとダイジェストを作成および代入する
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

メールを送信するメイラーを作る

$ rails generate mailer UserMailer account_activation password_reset

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout 'mailer'
end



app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset
    @greeting = "Hi"

    mail to: "to@example.org"
  end
end




app/views/user_mailer/account_activation.text.erb

Hi <%= @user.name %>,
Welcome to the Sample App! Click on the link below to activate your account:
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>

app/views/user_mailer/account_activation.html.erb

<h1>Sample App</h1>

<p>Hi <%= @user.name %>,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email) %>

emailの情報からuserのactivation_tokenを持ってきている。
activation_digestはカラムに入れたが、activation_tokenはカラムに入れてないから@userで持ってこれないからだと思う。
カラムに入れない理由はdbで情報を盗まれたら困るため。そのためハッシュ化したactivation_digestをカラムに入れてる。




送信メールのプレビュー
メールを送らずともメールの画面を確認できる。

config/environments/development.rb

Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = false

  host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。
  # クラウドIDEの場合は以下をお使いください
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  # localhostで開発している場合は以下をお使いください
  # config.action_mailer.default_url_options = { host: host, protocol: 'http' }
  .
  .
  .
end


localhostの場合は
host = 'localhost:3000'                     # ローカル環境
config.action_mailer.default_url_options = { host: host, protocol: 'http' }





test/mailers/previews/user_mailer_preview.rb

# Preview all emails at http://localhost:3000/rails/mailers/user_mailer

class UserMailerPreview < ActionMailer::Preview

# Preview this email at #

http://localhost:3000/rails/mailers/user_mailer/account_activation

def account_activation

user = User.first user.activation_token = User.new_token UserMailer.account_activation(user)

end #

Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end



test/mailers/previews/user_mailer_preview.rb


ユーザーのcreateアクションにメール送信記述を書く。

app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      UserMailer.account_activation(@user).deliver_now
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
  .
  .
  .
end

メールはローカル環境であればサーバーログ内(ターミナル内)で確認。
urlが書いてあるのでそれを持ってきて貼るとメールが確認できる。


authenticated?メソッド

app/models/user.rb

class User < ApplicationRecord
  .
  .
  .
  # トークンがダイジェストと一致したらtrueを返す
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end
  .
  .
  .
end

この形にしているのは、acctivation_digestの他に、クッキーのremember_digestでも使うため。
どちらでも対応できるようにするためsendメソッドを用いて、引数で送られてきたやつのdigestを取り扱っている。

app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 現在ログイン中のユーザーを返す(いる場合)
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end
引数を2つに変更する。




app/controllers/account_activations_controller.rb
メール内のedit_pathをクリックしたときのアクション
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated,    true)
      user.update_attribute(:activated_at, Time.zone.now)
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

activatedがtrueであれば、ログインできるようにsessionコントローラーに記述する def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) if user.activated? log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else message = "Account not activated. " message += "Check your email for the activation link." flash[:warning] = message redirect_to root_url end else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out if logged_in? redirect_to root_url end end


リファクタリング

# アカウントを有効にする
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end

# 有効化用のメールを送信する
def send_activation_email
UserMailer.account_activation(self).deliver_now
end


  private
    .
    .
    .
end
 
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
  .
  .
  .
end

app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
  end
end


 有効なユーザーだけを表示するコードのテンプレート
app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.where(activated: true).paginate(page: params[:page])
  end

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless activated?
  end
  .
  .
  .
end


本番環境でのメール送信

mailgunを使用する

 

config/environments/production.rb
Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '<あなたのHerokuサブドメイン名>.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :port           => ENV['MAILGUN_SMTP_PORT'],
    :address        => ENV['MAILGUN_SMTP_SERVER'],
    :user_name      => ENV['MAILGUN_SMTP_LOGIN'],
    :password       => ENV['MAILGUN_SMTP_PASSWORD'],
    :domain         => host,
    :authentication => :plain,
  }
  .
  .
  .
end
herokuのurlでok



$ rails test
$ git add -A
$ git commit -m "Add account activation"
$ git checkout master
$ git merge account-activation

$ rails test
$ git push
$ git push heroku
$ heroku run rails db:migrate

$ heroku addons:create mailgun:starter
MailgunのHerokuアドオンを追加する
$ heroku config:get MAILGUN_SMTP_LOGIN
$ heroku config:get MAILGUN_SMTP_PASSWORD
今回の場合は環境変数が自動で設定されるみたいなのでこれら必要ないみたい。

$ heroku addons:open mailgun

Mailgun ダッシュボードのURLが表示されるのでブラウザで開きます。

こちらで受信するメールアドレスを認証します。画面左側の「Sending」→「Domains」のリストにある「sandbox」で始まるサンドボックスドメインを選択します。画面右側の「Authorized Recipients」から受信メールアドレスを認証し、本番環境でのメール送信準備は完了です。

これは実際に存在するメールにしなければメールが来ない。今回であればgmailのメールを受信するようにした。
その結果登録したgmail 宛にメールが来た。
 






 
 
 


 


Railsチュートリアル9章 クッキー(remember me)

クッキー

第9章で学んだクッキーについてまとめる。

テストコードは省略する。

 

まずはmigration追加

 

$ rails generate migration add_remember_digest_to_users remember_digest:string

class AddRememberDigestToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :remember_digest, :string
  end
end
$ rails db:migrate



app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end
end
# これでランダムな文字列の組み合わせとしてトークンを作っている

# 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end
# 作成したremember_tokenをdigest化でハッシュにしてそれをremember_digestとして保存している。

# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token) BCrypt::Password.new(remember_digest).is_password?(remember_token) end
# ハッシュ化したremember_digest(dbのカラムに保存されてる)と引数として渡されたremember_tokenが合致するか確かめる。
   BCryptを使うことでハッシュ後とハッシュ前でも比較できる。






app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user 
      ヘルパー内の記述に飛ぶ
redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out redirect_to root_url end end






app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
#ここでクッキーに情報を入れている。 # 現在ログインしているユーザーを返す(いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end end






current_user変更
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 記憶トークcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
user_id = session[:user_id]はイコールではなく、代入。user_idはただの変数。
だから最初のifはsession[:user_id]があればでok,あったらuser_idに代入する。
なかったらelsif文にいく。
ここでauthencticated?を使ってクッキーにあるremember_tokenとdbのカラムに保存したrember_digestを比較してあげてる。

# ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end end





ユーザーを忘れる
app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end






app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController . . . def destroy log_out if logged_in? redirect_to root_url end end






app/models/user.rb

これはテストのせいで追加
class User < ApplicationRecord
  .
  .
  .
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end






[remember me]チェックボックス

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>






app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
チェックボックスにチェックを入れると1でremember,チェックを入れないと0でfoeget
redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out if logged_in? redirect_to root_url end end
 
 
 
 
 
 
 
 

Railsチュートリアル8章 セッション

セッション

セッションについてまとめる。テストコードは省略する。

 

コントローラー作成
rails generate controller Sessions new
ここでnewを使っているのはコントローラーと同時にnewのビューを作りたいため。
他のアクションはビュー必要ないので後で定義

ルーティング

get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
名前付きのルートを作っているが、resourcesでも問題はない
 
ビュー
app/views/sessions/new.html.erb

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

コントローラー

app/controllers/sessions_controller.rb

def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      

      #authenticateは入力したパスワードがハッシュ化されてdbにあるパスワードと合致するかをしている。

      authenticateはモデル内にhas_secure_passwordを記述したことで使えるようになる。

      has_secure_passwordを使うにはbycriptというgemが必要。

      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

ログインメソッド

ログインメソッドをヘルパー内に書く。

そしてそれをコントローラー内でも使えるようにapplicationコントローラーに入れ込む。

 

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include SessionsHelper
end

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

コントローラーに追加

loginメソッドを入れる

app/controllers/sessions_controller.rb

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

 現在のuser

app/helpers/sessions_helper.rb

def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

ログインしているかどうか(同じくhelper内)
def logged_in?
    !current_user.nil?
  end

ビュー
app/views/layouts/_header.html.erb

<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", '#' %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

ドロップダウンを作るためjavascriptを入れる。

config/webpack/environment.js

 

const { environment } = require('@rails/webpacker')

const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)

module.exports = environment

app/javascript/packs/application.js

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("jquery")
import "bootstrap"

Userモデル内

  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

現段階ではテストコードにしか使わないのでスルーでおk

 

ユーザー登録時にログインできるようにする

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(user_params)
    if @user.save
      log_in @user
      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


ログアウト

app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end