Try   HackMD

上一篇筆記:Astro課程 0723-0731 - Rails (Day1-Day4)

書籍推薦:Everyday Rails Testing with RSpec

會員註冊頁面

sign_up.html.erb

使用form_for(傳遞一個model)

<%= form_for(@user) do |form| %>
  <div class="fields">
    <%= form.label :account, "帳號" %>
    <%= form.text_field :account %>
  </div>

  <div class="fields">
    <%= form.label :password, "密碼" %>
    <%= form.password_field :password %>
  </div>

  <div class="fields">
    <%= form.label :email, "信箱" %>
    <%= form.email_field :email %>
  </div>

  <%= form.submit "註冊帳號" %>

<% end %>

controller

  def sign_up
    @user = User.new    
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirct_to root_path, notice: "會員註冊成功"
    else
      render :sign_up
    end
  end

sesion

http沒有狀態,不知道使用者有沒有登入
所以rails後台給瀏覽器一張session的票

  session[:user_token] = @user.id

:user_token後面會塞一些使用者的獨特訊息(eg.各自用的瀏覽器id),以判斷不同使用者

Eg.發文的時候才會檢查是否有登入 => 檢查票是否存在

  def create
    @user = User.new(user_params)
    if @user.save
      session[:user_token] = @user.id
      redirect_to home_users_path, notice: "會員註冊成功"
    else
      render :sign_up
    end
  end

註冊成功時show出navbar

    <nav>
      <ul>
        <% if session[:user_token]%>
          <li>account</li>
          <li>登出</li>
        <% else %>
          <li>登入</li>
          <li>註冊</li>
        <% end %>
      </ul>
    </nav>

在每一個頁面找出登入的user

每一頁都需要用到的功能,放在application controller

沒有註冊的話,還是要show出頁面

  before_action :find_user

  private
  def find_user
    if seesion[:user_token]
      @current_user = User.find(session[:user_token])
  end

view helper

視圖小幫手
form_forlink_to都是view helper

第一種方式:自己定義 helper

遇到同名的helper 後面的檔會蓋過前面(所以不需要的檔案就把它刪掉)

rails generator config: 自訂產生的預設檔案
rails template

module UsersHelper
  # 檢查使用者有沒有登入
  def user_signed_in?
    session[:user_token]
  end

  # 登入的話把session找出來
  def current_user
      User.find(session[:user_token]) if user_signed_in?
  end 
end

_navbar就可以使用current_user了!

<nav>
<%= hello %>
  <ul>
    <% if current_user %>
      <li><%= current_user.account%></li>
      <li><%= link_to '登出', sign_out_users_path, method: :delete %></li>
    <% else %>
      <li>登入</li>
      <li>註冊</li>
    <% end %>
  </ul>
</nav>

登入後才能開版

board controller加上條件判斷

  def new
    if user_signed_in?
      @board = Board.new
    else
      redirect_to sign_in_users_path, notice: "請先登入會員";
  end

第二種方式

寫在application controller,然後匯出給view用

  helper_method :user_signed_in?, :current_user 
  
    private
  def not_found
    render file: '/public/404.html', status: 404
  end

  def user_signed_in?
    # session繼承ActionController, 可以在controller和view用
    
    session[:user_token]
  end

  def current_user
    User.find(session[:user_token]) if user_signed_in?
  end

參考helper_method api,改成||的寫法

  def user_signed_in?
    current_user != nil
    #session[:user_token]
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_token])
    # User.find(session[:user_token]) if user_signed_in?
  end

使用者登出功能

  def sign_out
    session[:user_token] = nil
    redirect_to root_path, notice: "登出成功"
  end

進一步:把session包成private方法
增加程式碼可讀性

  private
  def sign_in_user(u)
    session[:user_token] = u.id
  end

  def sign_out_user
    session[:user_token] = nil
  end
  def create
    @user = User.new(user_params)

    if @user.save
      # 登入
      sign_in_user(@user)
      redirect_to root_path, notice: '會員註冊成功'
    else
      render :sign_up
    end
  end

  def sign_out
    sign_out_user
    redirect_to root_path, notice: '登出成功'
  end

使用者登入功能

我們想讓使用者輸入帳號email都可以登入

sign_in.html.erb

<h1>登入會員</h1>

<%= form_for(@user, url: login_users_path) do |form| %>
  <div class="fields">
    <%= form.label :account, "帳號" %>
    <%= form.text_field :account, placeholder: "請輸入帳號與密碼" %>
  </div>


  <div class="fields">
    <%= form.label :password, "密碼" %>
    <%= form.password_field :password %>
  </div>

  <%= form.submit "登入" %>

<% end %>

先看之前寫過的user.rb

  def self.login(options)
    if options[:account] && options[:password]
      find_by(account: options[:account],           
             password: add_salt(options[:password]))   # 已加密的密碼
    # else  => 要不然是true要不然是nil,所以可以註解掉
    #   return false
    end
  end

然後思考user controller

  def login
    # 寫params的話,用兩層 params[:user][:account]
    if user_params[:account] && user_params[:password] #有填寫的話
      # 確認有填寫之後要認證
      # Duck Typing: 長得看起來像Hash,所以就當作Hash來用(跟之前寫的option類似)
      user = User.login(user_params) # 兩種結果: user的資料或是nil 
      if user #有使用者的話
        sign_in_user(user)
      else #nil
        redirect_to sign_in_user_path, notice: "請輸入正確帳號密碼"        
      end
    else # 沒有填寫的話
    redirect_to sign_in_user_path, notice: "請輸入正確帳號密碼"        
  end

簡化:

  def login
    user = User.login(user_params)

    if user
      sign_in_user(user)
      redirect_to root_path, notice: "成功登入"
    else
      redirect_to sign_in_users_path, notice: "請輸入正確帳號密碼"
    end
  end

使用某些功能之前,必須請使用者要先登入的method

eg. 編輯、刪除看板

boards controller

  before_action :authenticate_user!, except: [:index, :show]

:authenticate_user!放在application controller裡,讓其他的controller也可以用

  def authenticate_user!
    redirect_to root_path, notice: "請登入會員!" if not user_signed_in?
  end

新增看版時,加入user

開新欄位:產生新欄位給board

rails g migration add_user_to_board user:belongs_to

但之前已建好的板user_id不存在

class AddUserToBoard < ActiveRecord::Migration[6.0]
  def change
    add_reference :boards, :user, null: false, foreign_key: true
    # add_column :boards, :user_id, :integer
  end
end

另一種方式:

多對多關聯

rails g model BoardMaster user:belongs_to board:belongs_to

board.rb has many users:

  has_many :board_masters
  has_many :users, through: :board_masters

user.rb has many boards:

  has_many :board_masters
  has_many :boards, through: :board_masters

進去console查詢多對多

2.5.2 :002 > b1 = Board.first
  Board Load (0.2ms)  SELECT  "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL ORDER BY "boards"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35"> 
 
2.5.2 :003 > b1.users
  User Load (0.3ms)  SELECT  "users".* FROM "users" INNER JOIN "board_masters" ON "users"."id" = "board_masters"."user_id" WHERE "board_masters"."board_id" = ? LIMIT ?  [["board_id", 2], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy []>
 

2.5.2 :001 > u1 = User.first
  User Load (0.7ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<User id: 1, account: "bb", password: "cf53685c123ae36dcfe5ef73faaf9ea769f86c16", email: "bb", nickname: nil, gender: nil, state: "normal", deleted_at: nil, created_at: "2020-08-02 09:08:53", updated_at: "2020-08-02 09:08:53"> 
 
2.5.2 :002 > u1.boards
  Board Load (0.8ms)  SELECT  "boards".* FROM "boards" INNER JOIN "board_masters" ON "boards"."id" = "board_masters"."board_id" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "board_masters"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy []> 

看板沒有版主的話,顯示為徵求中

view

<div>
  [版主:<%= display_bm(@board) %>]
</div>

建一個boards_helper.rb

module BoardsHelper
  def display_bm(board)
    if board.users.count == 0
      "徵求中"
    else
      board.users.map { |user| user.account }.join("/")
    end
  end
end

在console 指定版主

b1.users << u1

2.5.2 :001 > u1 = User.first
  User Load (0.7ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<User id: 1, account: "bb", password: "cf53685c123ae36dcfe5ef73faaf9ea769f86c16", email: "bb", nickname: nil, gender: nil, state: "normal", deleted_at: nil, created_at: "2020-08-02 09:08:53", updated_at: "2020-08-02 09:08:53"> 
2.5.2 :002 > u1.boards
  Board Load (0.8ms)  SELECT  "boards".* FROM "boards" INNER JOIN "board_masters" ON "boards"."id" = "board_masters"."board_id" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "board_masters"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy []> 
 
2.5.2 :003 > b1 = Board.first
  Board Load (3.2ms)  SELECT  "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL ORDER BY "boards"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<Board id: 2, title: "PHP", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:46:35", updated_at: "2020-07-23 07:46:35"> 

2.5.2 :004 > b1.users
  User Load (1.6ms)  SELECT  "users".* FROM "users" INNER JOIN "board_masters" ON "users"."id" = "board_masters"."user_id" WHERE "board_masters"."board_id" = ? LIMIT ?  [["board_id", 2], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy []> 

2.5.2 :005 > u1.boards
  Board Load (0.3ms)  SELECT  "boards".* FROM "boards" INNER JOIN "board_masters" ON "boards"."id" = "board_masters"."board_id" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "board_masters"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy []> 

2.5.2 :006 > b1.users << u1
   (0.7ms)  begin transaction
  BoardMaster Create (2.6ms)  INSERT INTO "board_masters" ("user_id", "board_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["user_id", 1], ["board_id", 2], ["created_at", "2020-08-03 09:16:55.601429"], ["updated_at", "2020-08-03 09:16:55.601429"]]
   (1.2ms)  commit transaction
  User Load (0.2ms)  SELECT  "users".* FROM "users" INNER JOIN "board_masters" ON "users"."id" = "board_masters"."user_id" WHERE "board_masters"."board_id" = ? LIMIT ?  [["board_id", 2], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, account: "bb", password: "cf53685c123ae36dcfe5ef73faaf9ea769f86c16", email: "bb", nickname: nil, gender: nil, state: "normal", deleted_at: nil, created_at: "2020-08-02 09:08:53", updated_at: "2020-08-02 09:08:53">]>