--- title: Astro課程 0803 - Rails (Day5) tags: astro, rails --- [上一篇筆記:Astro課程 0723-0731 - Rails (Day1-Day4)](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view) 書籍推薦:[Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) # 會員註冊頁面 `sign_up.html.erb` 使用form_for(傳遞一個model) ```htmlmixed <%= 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 ```ruby 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.發文的時候才會檢查是否有登入 => 檢查票是否存在 ```htmlmixed 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 ```htmlmixed <nav> <ul> <% if session[:user_token]%> <li>account</li> <li>登出</li> <% else %> <li>登入</li> <li>註冊</li> <% end %> </ul> </nav> ``` ## 在每一個頁面找出登入的user 每一頁都需要用到的功能,放在application controller 沒有註冊的話,還是要show出頁面 ```ruby before_action :find_user private def find_user if seesion[:user_token] @current_user = User.find(session[:user_token]) end ``` # view helper 視圖小幫手 `form_for` 和 `link_to`都是view helper ## 第一種方式:自己定義 helper 遇到同名的helper 後面的檔會蓋過前面(所以不需要的檔案就把它刪掉) [rails generator config: 自訂產生的預設檔案](https://guides.rubyonrails.org/generators.html) [rails template](https://github.com/kaochenlong/rails-template) ```ruby 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`了! ```htmlmixed <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加上條件判斷 ```ruby def new if user_signed_in? @board = Board.new else redirect_to sign_in_users_path, notice: "請先登入會員"; end ``` ## 第二種方式 寫在application controller,然後匯出給view用 ```ruby 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](https://apidock.com/rails/ActionController/Helpers/ClassMethods/helper_method),改成`||`的寫法 ```ruby 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方法 增加程式碼可讀性 ```ruby private def sign_in_user(u) session[:user_token] = u.id end def sign_out_user session[:user_token] = nil end ``` ```ruby 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 ```htmlmixed <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 ```ruby 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 ```ruby 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 ``` 簡化: ```ruby 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 ```ruby before_action :authenticate_user!, except: [:index, :show] ``` 把`:authenticate_user!`放在application controller裡,讓其他的controller也可以用 ```ruby 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">]> ```