CRUD

開啟終端機建立新rails專案 $rails new dcard
建立後$cd dcard進入專案資料夾
建立model$rails g model Board title

$rails db:migrate
migrate完成pic
$rails s啟動rails server連上http://localhost:3000 看到歡呼畫面
開啟vs code
連上http://localhost:3000/boards出現錯誤畫面pic缺少路徑
在vs code裡找到/DCARD/config/routes.rb
加上路徑 resources :boards
建立boards的七個路徑八個action
再回到http://localhost:3000/boards錯誤畫面換成缺少BoardsController pic
開啟新終端機$rails g controller Boards pic
錯誤畫面變成缺少action index pic
在BoardsController建立index方法
錯誤畫面變成缺少view
在/views/boards建立index.html/erb
在index.html/erb

<h1>看板標題</h1>
<%= link_to '新增看板', new_board_path%>

new_board_path為new的路徑(查表得知)

按下頁面的新增看板出現缺少new方法
在BoardsController建立new方法
在/views/boards建立new.html/erb

class BoardsController < ApplicationController
  def index
  end

  def new
  end
end

在new.html.erb做新增看板的表單

<h1>新增看板</h1>
<%= form_for(Board.new) do |board|%>
  <%= board.label :title ,"新增看板"%>
  <%= board.text_field :title %>
  <%= board.submit '新增'%>
<% end %>

按下新增後出現缺少create
在BoardsController增加create方法

class BoardsController < ApplicationController
  def index
  end

  def new
  end

  def create
    redirect_to '/'
  end
end

在這裡我們不需要create的view所以用redirect_to '/' 導回首頁
但是現在首頁是Rails的預設歡呼畫面
所以在routes.rb用root "boards#index"把首頁導向我們要的頁面boards/index
(root是rails用來引導首頁)

Rails.application.routes.draw do
resources :boards
root "boards#index"
end

接下來繼續完成create方法

def create
​​​ @board = Board.new(params[:board])
​​​ if @board.save
​​​   redirect_to '/'
​​​ else
​​​   render :new
​​​ end
​​​end

(params[:board]==>{"title" => 'ccc'})
params['board'] = params.require(:board)
再次新增看板後出現
ActiveModel::ForbiddenAttributesError in BoardsController#create
這是因為觸發rails內的保護機制
因為我們一次性把全部資料做輸入,其中可能有帶有惡意的資料
所以在這裡我們要對資料做清洗的動作

  def create
    clear_params = params.require(:board).permit(:title)
    @board = Board.new(clear_params)
    if @board.save
      redirect_to '/',notice: '新增成功'
    else
      render :new
    end
  end

clear_params = params.require(:board).permit(:title)
新設定一個變數clear_params指定為只接受board中的title
原來@board = Board.new(params[:board])中的params[:board]也就要改為clear_params
經過清洗後就可避免觸發保護機制
再來我們要設定新增看板的驗證機制
在/model/board.rb建立看板名稱必填並最少4字的規則

class Board < ApplicationRecord
  validates :title, presence: true,length: { minimum: 4 }
end

然後在create增加提示

    if @board.save
      redirect_to '/',notice: "新增成功"
    else

notice: "新增成功"為快閃訊息的省略寫法
(原寫法為flash["notice"] = "新增成功"
但目前這樣還不會顯示出快閃訊息
還要在/views下建立/shared/_flash.html.erb加上
<p><%= flash["notice"] %></p>
或簡寫<p><%= notice %></p>
這種_開頭的檔案稱為局部宣染檔案
要在/views/layouts/application.html.erb的body加上
<%= render "shared/flash" if flash[:notice] %>
在有快閃訊息時載入_flash.html.erb讀取路徑不用加_與副檔名
接下來我們希望新增失敗時可以保留失敗時輸入的值
我們已在create中在失敗時導回new.html.erb
new.html.erb中的<%= form_for(Board.new) do |board|%>
改為<%= form_for(@board) do |board|%>
現在會出現找不到@board的錯誤
我們再回到boards_controller的new方法

def new
​​  @board = Board.new
​​end

把@board指定為原來的Board.new
這樣看板新增失敗時就會保留原來輸入的值
這是因為我們刻意營造的巧合
原本new.html.erb的@board是代入new方法的@board = Board.new
但是在新增失敗時的@board
已經是create的@board = Board.new(clear_params)
正好就是我們輸入的值
接下來在index.html.erb中顯示我們已經新增的看板列表

<h1>看板標題</h1>
<%= link_to '新增看板', new_board_path %>
<ul>
  <% @board.each do |board| %>
    <li>
      <%= board.title %>
    </li>
  <% end %>
</ul>

目前會找不到@board因為還沒定義
所以在boards_controller的index

​def index
​   @board = Board.all
​ end

用Board.all取出全部看板
回到index.html頁面就會看到全部新增的看板
再來為看板增加連結讓點取看板時能連到個看板的頁面
<%= board.title %>加上連結改寫為
<%= link_to board.title, board_path(board.id)%>
board_path可以從查表得知,board.id則是各看板的id
然後點下看板名稱就會再出錯,因為又缺少了action 'show'
再回到BoardsController增加show方法

def show
​​  @board = Board.find(params[:id])
​​end

在/views/boards建立show.html.erb寫入

<h1><%= @board.title %></h1>

讓每個看板的頁面可以顯示看板名稱

接下來是修改功能
回到index頁面
在看板名稱前一行寫<%= link_to '修改', edit_board_path(board.id)%>
路徑edit_board_path同樣查表得知
點擊修改再次出現缺少edit的錯誤
回到BoardsController建立edit方法

​def edit
​​  @board = Board.find(params[:id])
​​end

在/views/boards建立edit.html.erb寫入

<h1>編輯看板</h1>
<%= form_for(@board) do |board|%>
  <%= board.label :title ,"編輯看板"%>
  <%= board.text_field :title %>
  <%= board.submit '編輯' %>
<% end %>

其實跟new.html.erb幾乎一樣,只是把新增改成編輯
回到edit頁面按下編輯又出錯缺少action 'update'
回到BoardsController建立update方法

​​def update
​​  @board = Board.find(params[:id])
​​  clear_params = params.require(:board).permit(:title)
​​  if @board.update(clear_params)
​​    redirect_to '/',notice: "編輯成功"
​​  else
​​    render :edit
​​  end
​​end

又跟create很像

再來是刪除功能
回到index頁面
在看板名稱前一行寫
<%= link_to "刪除",board_path(board.id) ,method: 'delete'%>
路徑board_path同樣查表得知
點擊修改再次出現缺少destroy的錯誤
回到BoardsController建立destroy方法

  def destroy
    @board = Board.find(params[:id])
    @board.destroy
    redirect_to root_path ,notice: "刪除成功"
  end

刪除功能相對單純許多只要用@board.destroy就刪除了
刪除後再導回首頁,root_path 是rails內建的首頁路徑
現在只要一按刪除就真的刪除了
為了避免誤觸再追加一個提示訊息data: {confirm: "是否刪除"}%>
加在index.html.erb的刪除連結後
<%= link_to "刪除",board_path(board.id) ,method: 'delete', data: {confirm: "是否刪除"}%>
這樣一來每次點擊刪除都會出現是否刪除的提示
=================================
Board.find(1)只能接數字
Board.find_by(id: 1,email: 'aaa@aaa.aa')
find_by(搜尋的欄位: 欄位的值)
Board.find_by(id: 1)意思為從id欄位中找尋值為1的Board
(id: 1,email: 'aaa@aaa.aa')是個hash
Board.where(id: 1,email: 'aaa@aaa.aa')類似find_by
find,find_by只能找一筆資料,where可以找多筆資料回傳的資料是陣列
在查詢不到資料時find會出現錯誤,find_by會回傳nil不會報錯,find_by!才會報錯,而where會回傳一個空陣列也不會報錯
=================================

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
  private
    def record_not_found
      render file: '/public/404.html' ,layout: false,status: 404
    end
end

rescue_from ActiveRecord::RecordNotFound with: :record_not_found
原來寫法rescue_from(情況 {with: :方法名稱})
(情況 {with: :方法})是參數
{with: :方法}是個HASH
在這裡是指遇到ActiveRecord::RecordNotFound時用record_not_found方法進行處理
record_not_found內容為讀取404.html,取消公版的layout,回報網頁瀏覽懶器狀態為404
寫在ApplicationController裡可以讓所有Controller使用
=================================

<% if @board.errors.any? %>
  <ul>
    <% @board.errors.full_messages.each do |message| %>
    <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

可以寫在new和edit頁面,在新增或修改失敗時列出錯誤訊息
=================================
整理程式碼
可以發現show,edit,update,destroy都有
@board = Board.find(params[:id])
所以可以在private新增成一個方法

private
​   def find_board
​     @board = Board.find(params[:id])
​   end

然後在最前面加上
before_action :find_board, only: [:show, :edit , :update , :destroy]
這樣在指定的action都會執行@board = Board.find(params[:id])
同樣clear_params = params.require(:board).permit(:title)有重覆使用也可以在private建立方法

def clear_params
​     params.require(:board).permit(:title)
​   end

這裡不用before_action是因為create和update裡的clear_params已經直接呼叫方法了
show,edit,update,destroy尤其是show,edit已經空空如也,不用before_action根本不知道要做什麼

再來new.html和edit.html也幾乎一樣,就在同資料夾的/boards建立_form.html.erb貼上

<% if board.errors.any? %>
  <ul>
    <% board.errors.full_messages.each do |message| %>
    <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

<%= form_for(board) do |board| %>
  <%= board.label :title , "看板名稱" %>
  <%= board.text_field :title %>
  <%= board.submit %>
<% end %>

但我們把@board全部拿掉@改成board,因為局部宣染的檔案會使用多次,所以在呼叫使用時再塞入資料,才能正確呈現每個頁面,讓局部宣染的檔案自己抓資料可能抓不到該頁面要用的資料
在new.html和edit.html用render呼叫局部宣染_form.html.erb
<%= render 'form' %>
這時候會出錯,因為我們把@拿掉了,要把@board重新補回去
<%= render 'form' ,board: @board%>

在index,html中
<%= link_to board.title, board_path(board.id)%>
.id是可以省略的,就變成
<%= link_to board.title, board_path(board)<%= link_to board.title, board_path(board)
又因為是查詢同名的單筆資料board_path(board)可以再省略成board
最後就變成<%= link_to board.title, board %>
同理
<%= link_to "刪除",board_path(board.id) ,method: 'delete', data: {confirm: "是否刪除"}%>
也可以省略成
<%= link_to "刪除", board ,method: 'delete', data: {confirm: "是否刪除"}%>

=======================================
註冊 登入 功能
先在routes.rb

Rails.application.routes.draw do
  root "boards#index"
  
  get '/users/sign_up', to: 'registrations#new'
  
  resources :boards
end

建立註冊路徑get '/users/sign_up', to: 'registrations#new'
這裡生成的路徑為users_sign_up_path也可以做更改
get '/users/sign_up', to: 'registrations#new, as: 'registration'
這樣一來路徑就會變成registration_path
在nvabar就可以新增註冊的聯結
<li><%= link_to '註冊',registration_path %></li>

連線到http://localhost:3000/users/sign_up
出現uninitialized constant RegistrationsController
我們可以在vs code手動建立或用$rails g controller Registrations建立RegistrationsController
接下來錯誤會變成缺少new action和new.html頁面
直接在registrations_Controller.rb

class RegistrationsController < ApplicationController
  def new
  end
end

以及在views下新增資料夾registrations及new.html.erb

$rails g model User email password nickname建立使用者User的model裡面有email password nickname三個欄位

在new.html.erb裡

<h1>註冊會員</h1>
<%= form_for(@user) do |form| %>
  <div class="field">
    <%= form.label :email ,'帳號'%>
    <%= form.text_field :email %>
  </div>
  <div class="field">
    <%= form.label :password ,'密碼'%>
    <%= form.password_field :password %>
  </div>
  <div class="field">
    <%= form.label :password_confirmation ,'密碼確認'%>
    <%= form.password_field :password_confirmation %>
  </div>
  <div class="field">
    <%= form.label :nickname ,'暱稱'%>
    <%= form.text_field :nickname %>
  </div>
  <%= form.submit '註冊' %>
<% end %>

密碼使用password_field可以變成星號顯示
password_confirmation是驗證密碼的欄位
出現NoMethodError in Registrations#new
因為@user預設路徑users_path對應users#create,但從路徑對照表上可以發現並沒有這條路徑,我們是使用registrations_controller而不是user_controller
所以我們直接在routes.rb新增路徑給他post '/users', to: 'registrations#create'
再到路徑對照表就可以發現出現users_path對應registrations#create

輸入資料按下註冊後出現錯誤缺少create action
再回到registrations_controller加上create

 def create
    @user = User.new(clear_user)
    if @user.save
      redirect_to '/',notice: "註冊成功"
    else
      render :new
    end
  end

  private
    def clear_user
      params.require(:user).permit(:email, :password ,:password_confirmation, :nickname)
    end

類似前面board的create,要加入:password_confirmation,否則不會驗證,這裡的new會找同名controller資料夾的new

接下來我們用$rails c進入rails console
$User.first來看第一個使用者的資料
可以看到password: [FILTERED],FILTERED只是rails隱藏起來的表示方法,實際上並沒有加密密碼,用SQLite打開資料庫就可以一覽無遺


在user model加上驗證規則,email password nickname都為必填,另外email必須是唯一

class User < ApplicationRecord
  validates :email, presence: true ,uniqueness: true, format: { with: /[\w]+@([\w-]+\.)+[\w-]{2,4}/ } 
  validates :password, presence: true, confirmation: true
  validates :nickname, presence: true
end

format是rails中的一種驗證器,後面接的一長串稱為常規表示法
在password加上confirmation: true就會驗證password與password_confirmation是否相同(慣例)

再來做密碼加密
在routes.rb

 before_create :encrypt_password

  private
  def encrypt_password
    self.password = Digest::SHA1.hexdigest("a#{self.password}z")
  end

之所以用before_create是因為密碼只有在註冊時會加密一次,如果用before_save會變成每次有資料存入都會加密一次
===========================
登入
先在user.rb建立路徑

​get '/users/sign_in', to: 'sessions#new', as: 'session'
​post '/login', to: 'sessions#create', as: 'login'

然後再路徑對照表中找到session_path對應sessions#new
所以就可以將<li><%= link_to '登入', session_path %></li>
加入_navbar.html.erb,點擊登入後會出現錯誤uninitialized constant SessionsController,所以我們就要建立SessionsController,然後又是前面做過的建立new action與new.html.erb
在new.html.erb中

<h1>會員登入</h1>
<%= form_for(@user) do |form| %>
  <div class="fields">
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>
  <div class="fields">
    <%= form.label :password , '密碼' %>
    <%= form.password_field :password %>
  </div>
  <%= form.submit '登入'%>
<%end %>

然後又出錯ArgumentError in Sessions#new
所以在SessionsController中

  def new
    @user = User.new
  end

但是我們在這裡並沒有要真的新增使用者,只是為了消除ArgumentError in Sessions#new而已
<%= form_for(@user) do |form| %>中會照CRUD方式走向users_path,但實際上我們要走的從路徑對照表中得知是login_path對應sessions#create,所以
<%= form_for(@user) do |form| %>
要加上url變成
<%= form_for(@user, url: 'login_path', method: 'post') do |form| %>

==============
form_for(MODEL)
form_tag(url)
上面兩個都會做出表單,form_for一定要接model,form_tag就處理與model無關的
在rails 5之後出現form_with來取代,以AJAX來傳遞資料(js會介紹)
form_with(model: @user)
form_with(url: '/')
==================
又可以改成<%= form_with(model: @user, url: 'login_path', method: 'post') do |form| %>
(不過在這裡其實只是要比對資料而沒有要真的新增user)
按下登入後畫面靜止不動(其實是有動從終端機可看到錯誤ActionNotFound

(The action 'create' could not be found for SessionsController),這是因為form_with的AJAX,可以再加上local: true來使用post方式傳遞資料,所以寫成
<%= form_with(model: @user, url: login_path, method: 'post', local: true) do |form| %>
就會出現習慣的紅色錯誤畫面ActionNotFound
在SessionsController建立

 def create
    @user = User.find_by(email: params[:user][:email],
                         password: params[:user][:password])
    if @user
      session[:user9527] = params[:user][:email]
      redirect_to root_path, notice: '登入成功'
    else
      redirect_to session_path, notice: '登入失敗'
    end
  end

因為find只能接id,所以我們在這用find_by,也不用find_by!因為找不到會出現404,但只需要導回登入頁重填即可
session是一個特別的東西,他會在伺服器建一個session同時發一個cookie給使用者,在這裡我們意思是發一張名為user9527的號碼牌,而號碼牌的內容為使用者user的email(但有session不代表有登入,登入是一個有點複雜的狀態,html不懂登入是什麼)
我們又會發現到即使輸入剛剛註冊的帳號也仍然會登入失敗,這是因為資料庫裡儲存的事已經加密的密碼,而我們在create中找的則是直接輸入為加密的密碼,所以要加上
pw = Digest::SHA1.hexdigest("a#{params[:user][:password]}z")
password: params[:user]也要改成password: pw
所以整個create就變成

  def create
    pw = Digest::SHA1.hexdigest("a#{params[:user][:password]}z")
    @user = User.find_by(email: params[:user][:email],
                         password: pw)
    if @user
      session[:user9527] = params[:user][:email]
      redirect_to root_path, notice: '登入成功'
    else
      redirect_to session_path, notice: '登入失敗'
    end
  end

就可以正常登入了
再來我們來整理程式碼,因為在controller應該要只負責取東西,所以我們把商業邏輯有關的放model也可以方便重覆運用,所以把

pw = Digest::SHA1.hexdigest("a#{params[:user][:password]}z")
@user = User.find_by(email: params[:user][:email],
​                        password: pw)

放在user.rb定義成login方法

​​def self.login(u)
​​  pw = Digest::SHA1.hexdigest("a#{u[:password]}z")
​​  User.find_by(email: u[:email], password: pw)
​​end

然後create就可以改成

​​def create
​​  if User.login(params[:user])
​​    session[:user9527] = params[:user][:email]
​​    redirect_to root_path, notice: '登入成功'
​​  else
​​    redirect_to session_path, notice: '登入失敗'
​​  end
​​end

在執行create時會代入params[:user]到user model裡的類別方法,用u來承接create的params[:user]
邏輯不要放在controller裡,controller只用來取東西,越乾淨越好

========================
登出
在routes.rb新增路徑
delete '/users/sign_out', to: 'sessions#destroy', as: 'logout'
修改_navbar.html.erb

<ul>
  <% if session[:user9527].present? %>
    <li> <%= link_to "登出", 'logout_Path',method: 'delete' %> </li>
  <% else %>
    <li><%= link_to '註冊',registration_path %>  </li>
    <li><%= link_to '登入', session_path %></li>
<% end %>

判斷session[:user9527]存在時顯示登出否則顯示登入與註冊,另外登出要加上method: 'delete'則預設會使用get
現在我們就完成註冊登入登出囉

====================
我們還可以再整理程式碼
在SessionsHelper.rb

  def current_user
    if session[:user9527].present?
      User.find_by(email: session[:user9527])
    else
      nil
    end
  end

建立一個current_user方法,在session[:user9527]存在時,撈出email與session[:user9527]內email相同的使用者,否則為nil
所以_navbar.html.erb可以將session[:user9527].present?改成current_user就變成

      <ul>
        <% if current_user %>
          <li><%= current_user.nickname  %></li>
          <li><%= link_to "登出", logout_path,method: 'delete' %></li>
        <% else %>
          <li><%= link_to '註冊',registration_path %></li>
          <li><%= link_to '登入', session_path %></li>
        <% end %>
      </ul>

current_user.nickname則可以印出使用者的nickname
但在這情況下每次頁面都會重新執行User.find_by(email: session[:user9527])再撈一次資料(n+1問題),所以我們可以用||=改寫current_user方法

    if session[:user9527].present?
      @user ||= User.find_by(email: session[:user9527])
    else

只有第一次執行時會進行撈資料的動作,同時將其值指定給@user,之後執行current_user就會直接回傳@user而不會再撈資料
a = b => a = a b,a沒有被初始化,或為nil或false時將b值指定給a,其他情況維持a的原值

1120 0517 devise
============
會員資料編輯
在routes.rb建立新的路徑

get "/users/edit", to: "registrations#edit", as: 'edit_registration'
put "/users/edit", to: "registrations#update", as: 'update _registration'

又發現可以用resource :user, controller: 'registrations'也可以做出以上兩個路徑,再仔細看路徑對照表,可以看到也做出與post '/users', to: 'registrations#create'同樣的路徑,最後可以整理寫成
resource :user, controller: 'registrations',only: [:create, :edit, :update]
再來get '/users/sign_up', to: 'registrations#new', as: 'registration'也同樣使用registrations_controller也可以一並整理進來變成

  resource :user, controller: 'registrations',only: [:create, :edit, :update] do 
     get '/sign_up', action: 'new' 
  end

這樣修改後經由路徑對照表發現註冊的路徑也必需改成
<li><%= link_to '註冊',sign_up_user_path %></li>

再整理以下三行

​get '/users/sign_in', to: 'sessions#new', as: 'session'
​post '/login', to: 'sessions#create', as: 'login'
​delete '/users/sign_out', to: 'sessions#destroy', as: 'logout'

但是這三行/users/sign_in和/login與/users/sign_out都不是經典的action,所以only放空陣列,將這三行全部改寫放進Block,其中login為了統一性在特別改寫成sign_in,
最後如下

​resource :users, controller: 'sessionss',only: [] do 
​   get '/sign_in', action: 'new'
​   post '/sign_in', action: 'create' 
​   delete '/sign_out', action: 'destroy'
​ end

但是這樣一來,登入與登出的路徑也要修改如下

<li><%= link_to "登出", sign_out_users_path,method: 'delete' %></li>
<li><%= link_to '登入', sign_in_users_path %></li>

在/sessions下的會員登入new.html.erb的也要改url
<%= form_with(model: @user, url: sign_in_users_path, method: 'post', local: true) do |form| %>
開啟rails s,連至http://localhost:3000/users/edit
發生錯誤The action 'edit' could not be found for RegistrationsController
在RegistrationsController建立edit action
在views/registrations建立edit.html.erb

<h1>編輯個人資料</h1>
<%= form_for(current_user,url: users_path, method: 'put') do |form| %>
  <div class="field">
    <%= form.label :email ,'email'%>
    <%= form.text_field :email %>
  </div>
  
  <div class="field">
    <%= form.label :nickname ,'暱稱'%>
    <%= form.text_field :nickname %>
  </div>
  <%= form.submit '更新' %>
<% end %>

一般使用者不需要知道id所以一開始是用resource不帶s來新增路徑,刀錢使用者則使用helper裡的current_user,同樣他不是經典的action,要從路徑對照表找出對應的路徑,添加url和method
按下更新後出現錯誤The action 'update' could not be found for RegistrationsController
建立update action

  def update
    if current_user.update(clear_user)
      redirect_to edit_users_path, notice: '更新成功'
    else
      
    end
  end

按下更新後出現錯誤undefined local variable or method current_user
這是因為current_user 是寫在SessionsHelper裡,而預設是view專屬的,在model和controller中都取不到
到sessions_helper.rb裡

module SessionsHelper
  def current_user
    if session[:user9527].present?
      @user ||= User.find_by(email: session[:user9527])
    else
      nil
    end
  end
end

可以發現SessionsHelper是一個模組(module),所以可以在RegistrationsController中用include SessionsHelper,如果發現current_user很常被使用可以將include SessionsHelper寫在上一層的ApplicationController,這樣其下的controller都可以使用到current_user
修改email後按下更新出現錯誤ArgumentError in Registrations,因為我們的session當初是帶入email的,更新email之後就找不到了,用更新的email可重新登入
所以我們應該把session的內容改成使用者id,修改create

​ def create
​   user = User.login(params[:user])
​   if user
​     session[:user9527] = user.id
​     redirect_to root_path, notice: '登入成功'
​   else
​     redirect_to sign_in_users_path, notice: '登入失敗'
​   end
​ end

SessionsHelper也是放入email所以也要改成id
@_user9487 ||= User.find_by(id: session[:user9527])
這樣一來修改email就不會出錯了

接下來考慮到未登入時不該能使用編輯會員資料功能,所以在RegistrationsController裡加上 before_action :session_required, only: [:edit, :update]
session_required是自定義的判斷是否登入方法,在這裡只要求edit和update前要使用,至於session_required可以先寫在ApplicationController的private裡(要寫在哪憑經驗判斷,要讓每個需要的地方都能取用)

def session_required
  redirect_to sign_in_users_path, notice: '請先登入' if not current_user
end

如果current_user不存在,則回到登入頁面並提示請先登入(if not可以用unless)
============================
在看板新增文章
先在routes.rb新增路徑,因為是在看板下寫文章,路徑也要在看板下才合理,所以在原來的resources :boards下擴充posts路徑

resources :boards do
  resources :posts, shallow: true
end

從路徑對照表對應posts#new的路徑為new_board_post_path,如果只小這樣還是會出錯required keys: [:board_id],因為不知道文章要加在哪個board上,所以要再加上@board
在boards/show.html.erb

<h1><%= @board.title %></h1>
<%= link_to '新增文章', new_board_post_path(@board) %>

接下來按下新增文章就會出現uninitialized constant PostsController
要新增PostsController與new action同時在views下建立new.html.erb
接下來要建立post的model欄位有title、content、borad_id、user_id,可以用belongs_to來建立borad_id、user_id,如果打成board_id:belongs_to會建立board_id_id的欄位
$rails g model Post title content:text board:belongs_to user:belongs_to
在migration可以看到belongs_to的效果

t.belongs_to :board, null: false, foreign_key: true
t.belongs_to :user, null: false, foreign_key: true

在post.rb

class Post < ApplicationRecord
  belongs_to :board
  belongs_to :user
end

增加文章驗證,要有標題title與內文content

validates :title, presence: true
validates :content, presence: true

執行$rails db:migrate

在posts的new.html.erb增加表單

<h1>新增文章</h1>
<%= form_for(@post) do |form| %>
  <div class="field">
    <%= form.label :title ,'文章標題'%>
    <%= form.text_field :title %>
  </div>
  <div class="field">
    <%= form.label :content%>
    <%= form.text_area :content%>
  </div>

  <%= form.submit '新增文章' %>


<% end %>

也要建立new action

class PostsController < ApplicationController
  def new
    @post = Post.new
  end
end

再來還會報錯NoMethodError in Posts#new,因為post是建立在board下,所以以原來的CRUD規則會找不到路徑,要從路徑對照表找對應posts#create的是board_posts_path,同時還必須代入board的id,所以new.html.erb的@post改寫為

<h1>新增文章</h1>
<%= form_for(@post, url: board_posts_path(@board)) do |form| %>
  ......
<% end %>

還要在PostsController建立@board否則會找不到

  def new
    @post = Post.new
    @board = Board.find(params[:board_id])
  end

按下新增後出現The action 'create' could not be found for PostsController
建立create action

從log可以找到Parameters: {"authenticity_token"=>"oT3XwEIhL9S997ntl4LcC8+XRHw9cvWySCmdwBJ3SxWRoaxdIdDsa0o/fw7um0bNF4FP8vQUV+cvKz5cAwwKpA==", "post"=>{"title"=>"erth", "content"=>"argh"}, "commit"=>"新增文章", "board_id"=>"42"}
而我們只需要{"title"=>"erth", "content"=>"argh"}所以要清洗

private
  def post_params
    params.require(:post).permit(:title, :content)
  end

create可寫成

def create
  @post = Post.new(post_params)
    
  if @post.save
    redirect_to root_path, notice: '新增文章成功'
  else
    render :new
  end
end

但是仍然會失敗,用$rails c-- sanbox模擬
$p1 = Post.new(title: 'aaa', content: 'ccc')
$p1.save
==>false
得到false
$p1.errors.full_messages印出錯誤訊息
得到["Board must exist","User must exist"]
所以我們必須提供board_is和user_id

@post.board = @board
@post.user = current_user

有current_user就表示要驗證使用者是否登入
before_action :session_required, only: [:create]

換個角度
在user.rb
has_many :posts
每個使用者可以有很多文章,同理看板也是可以有很多文章
在board.rb
has_many :posts
has_many可以讓uesr跟board增加.posts功能
在rails c sandbox中測試
可以直接用user(或board)的id在post中找資料

$u1 = User.first
$u1.posts
 SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? LIMIT ?  
 [["user_id", 1], ["LIMIT", 11]]
 => #<ActiveRecord::Associations::CollectionProxy []> 

所以posts_controller中create原來的(普通寫法)

    @post = Post.new(post_params)
    @post.board = @board
    @post.user = current_user

可以改成以board角度的

    @post = @board.posts.new(post_params)
    @post.user = current_user

或是以user角度的

    @post = current_user.posts.new(post_params)
    @post.board = @board 

這兩種才是熟練關聯性的寫法

接下來就可以新增文章了~~
另外一開始在posts_controller中新增後回到首頁

if @post.save
  redirect_to root_path, notice: '新增文章成功'
else

我們可以改成回到文章看板

if @post.save
  redirect_to board_path(@board), notice: '新增文章成功'
else

又因為board_path(@board)名稱相同可以用@board表示

if @post.save
  redirect_to @board, notice: '新增文章成功'
else

接下來在看板顯示文章列表
在boards_comtroller的show中

@posts = Post.where(board: @board)
board: 是由belongs_to產生
@posts = Post.where(board_id: @board.id)

以上兩種寫法較差

@posts = @board.posts
熟練的寫法(@board是之前before action已經建立)

在boards的show.html.erb

<ul>
  <% @posts.each do |post| %>
      <li> <%= post.title %> </li>
  <% end %>
</ul>

但是目前顯示的文章會是最新的在最下方
所以要在show action用order增加逆向排序

def show
​ @posts = @board.posts.order(id: :desc)
end

再來我們希望點擊文章標題可以連到文章內容,所以要給文章標題增加連結
<li> <%= link_to post.title,post_path(post) %> </li>
post_path(post)可在簡寫成post(同名),最後寫成
<li> <%= link_to post.title,post %> </li>
點擊文章標題後出現The action 'show' could not be found for PostsController
建立show action與show.html.erb

def show
  @post = Post.find(params[:id])
end

在show.html.erb

<h2> <%= @post.user.nickname %></h2>
<h1> <%= @post.title %> </h1>
<%= if @post.user == current_user %>
  <%= link_to '編輯', 'edit_post_path(@post)' %>
<% end %>
<article>
  <%= simple_format(@post.content) %>
</article>
<%= link_to '回上一頁', :back %>

@post.user.nickname顯示文章作者暱稱
simple_format()一個helper讓文章換行,因為html只負責輸出畫面,對於空白或括弧沒有處理語法
:back是回上一頁的特殊寫法

<%= if @post.user == current_user %>
  <%= link_to '編輯', 'edit_post_path(@post)' %>
<% end %>

讓文章作者才可看到編輯選項
盡可能不要再view有出現判斷式,最好用方法包起來
所以改成
<%= if current_user.own?(@post) %>

<%= if @post.owned_by?(current_user) %>
  <%= link_to '編輯', 'edit_post_path(@post)' %>
<% end %>

又單行時if可以寫到後方
<%= link_to '編輯', edit_post_path(@post) if @post.owned_by?(current_user) %>
方法建立在post.rb

 def owned_by?(user)
   self.user == user
 end

self.user中的.user為belongs_to :user產生的方法
再來出現CRUD的缺少edit action與頁面
在posts_controller

def edit
  @post = Post.find(params[:id])
end

發現同show方法
所以可以在privat建立

def set_post
  @post = Post.find(params[:id])
end

講show跟edit用before_action整理(update預期會用上)
before_action :set_post, only: [:show, :edit, :update]
同時在before_action :session_required也要加上edit與update,未登入不可編輯和更新
在edit.html.erb借用新增文章new的form稍作修改

<%= form_for(@post) do |form| %>
  <div class="field">
    <%= form.label :title ,'文章標題'%>
    <%= form.text_field :title %>
  </div>
  <div class="field">
    <%= form.label :content%>
    <%= form.text_area :content%>
  </div>
  <%= form.submit '更新文章' %>
<% end %>

不用寫url因為觀察網址剛好符合CRUD,所以符合預設路徑
但是到目前為止有漏洞,只有檢查是否登入沒有檢查是否為作者,只要被使用者猜到網址就可以讓非文章作者進入編輯文章頁面,所以
before_action :set_post, only: [:show]
要拔掉edit與update
重新寫edit action

def edit
  @post = Post.find_by(:id params[:id])
end

但這樣只能找到要編輯的文章,還沒確認使用者,所以

def edit
  @post = Post.find_by!(id: params[:id], user: current_user )
end

加上驚嘆號,讓找不到時讓上層controller捕捉噴錯404.html
更好的寫法以使用者角度
@post = current_user.posts.find(params[:id])
點擊更新文章噴錯The action 'update' could not be found for PostsController
在PostsController裡

def update
  @post = current_user.posts.find(params[:id])
  if @post.update(post_params)
    redirect_to @post, notice: '更新成功'
  else
    render :edit
  end
end

============================
慣例

resources :accounts do
  resources :profiles, only[:index, :new, :create]
end
resources :profiles, only[:show, :edit, :update, :destroy]

以上profiles的action分類很常見所以有慣例寫法

resources :accounts do
  resources :profiles, shallow: true
end

另外擴充路徑還有member與collection兩種

resources :profiles, only[] do
  member do
    get :say_hello
  end
end
#做出profiles/3/say_hello 帶id

但是用resource不加s時,使用member一樣不會有id

resources :profiles, only[] do
  collection do
    get :say_hello
  end
end
#做出profiles/say_hello 不帶id

namespace用法

 namespace :admin do
   namespace :v1 do
     resources :user
   end
 end
#做出/admin/v1/user

============================
後續增加欄位
$rails g migration add_email_to_users
add_email_to_users不是固定寫法,只是一個檔名方便辨識加了什麼
執行後會在migrate資料夾做出一個空的檔案xxxxxxxxx_add_email_to_users.rb

class AddEmailToUsers < ActiveRecord::Migration[6.0]
  def change
  end
end

加上想增加的項目add_column(model名稱,新增欄位,型態)

def change
​ add_column(:users, :email, :string)`
​end

記得存檔後再執行$rails db:migrate
在schema.rb就可以看到新的email欄位建立好

create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "password"
    t.string "nickname"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "email"
  end

=============================
遇到專案沒有migration時可以用$rails db:schems:load