# 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`