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