tingtinghsu
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- title: Astro課程 0723-0731 - Rails (Day1-Day4) tags: astro, rails --- # 一起來做BBS看板! ## Rails專案實作快速連結 [0723 Day1 建立看板](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#0724-%E7%AC%AC%E4%BA%8C%E5%A4%A9) [0724 Day2 第一個Model: 新增看板](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#0724-%E7%AC%AC%E4%BA%8C%E5%A4%A9) [0730 Day3 第二個Model: 新增文章](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#%E7%AC%AC%E4%B8%89%E5%A4%A9) [0731 Day4 資料驗證、第三個Model: 新增使用者](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view#0731-%E7%AC%AC%E5%9B%9B%E5%A4%A9) ## 提問時間 <200> Q:網頁200 202 300 404......等是什麼意思? A:[HTTP狀態碼](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status) [Ref:傳紙條的故事](https://lidemy.com/courses/net101-js/lectures/9655079) < Type > Q:Type是什麼意思? A: ex:`'Content-Type' => 'text/html'`告訴瀏覽器將這個網頁以html來看待 # 專案主題發想 V PTT forum / comment / message / mail - 專案管理系統 Jira / Trello - 記帳 - Forum / Reddit - 購物網站 - Email https://hey.com/how-it-works/ - Flowdock / Slack / Line / Tinder (ActionCable - WebSocket) - Podcast 平台 - POS系統 (iPad) - https://www.ichefpos.com/zh-tw/delivery-integration - https://sharelike.asia/ - TimeTree RWD - https://timetreeapp.com/ - GitHub 目前比較不適合的專案 - Netflix 可以做的部分:前端、切版 - HackMD md -> html,即時互動 - Blog Medium - 口罩地圖 / 等公車 / 前端 - 購物網站 - Game RPG Maker / Unity (C++) - Facebook # PTT功能 - 公佈欄 - 我的最愛 - 分類 - 信件 - 聊天 - 個人設定 UserInfo ## 新增首頁 1. 改routes ``` Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html root "pages#index" get '/about', to: "pages#about" end ``` 2. rails g controller ``` pages_controller.rb # 檔名是蛇式 class PagesController # 類別名稱是駝峰式 ``` 3. 在pages下新增view檔案 # Board看板 Board的資料欄位 ``` - title:string - intro:text - deleted_at:datetime, nil/null (知道何時被刪除) -> 加index增加搜尋速度, 但會減慢寫入速度 (- is_deleted:boolean 是否被刪除, 預設false, 可以用deleted_at取代) - state: [normal, disable, hidden] ``` SQL: 列出所有(沒被刪除)的看板 ``` select * from boards where deleted_at is not null ``` ## 建立model ``` rails g model Board title:string intro:text deleted_at:datetime:index state:string ``` 以上指令會建出 `app/models/board.rb` ``` board.rb # 檔名是蛇式 class Board # 類別名稱是駝峰式 ``` 在migrate的create_boards.rb 把state設為normal ``` t.string :state, default: 'normal' ``` `rails db:migrate` ``` == 20200723061253 CreateBoards: migrating ===================================== -- create_table(:boards) -> 0.0012s -- add_index(:boards, :deleted_at) -> 0.0008s == 20200723061253 CreateBoards: migrated (0.0021s) ============================ ``` `rails db`指令可以進去sqlite查詢資料表 ``` rails db SQLite version 3.28.0 2019-04-15 14:49:49 Enter ".help" for usage hints. sqlite> select * from schema_migrations; 20200723061253 ``` Q. 如果執行`migration`後要再修改欄位: - 多人協作 `rails g migration [name]`--> rename_column 重新執行`rails db:migration` - 一個人做 `rails db:rollback` Q. 沒有存檔,欄位還沒更新就執行`rails db:migrate`,怎麼抓錯誤? -> 查看`schema.rb`,看看欄位有沒有被更新過 ## console模式新增資料 進`rails c`查看目前建立的Board ```  /PPT   master  rails c Running via Spring preloader in process 1560 Loading development environment (Rails 5.2.4.3) 2.5.2 :001 > Board.all Board Load (2.6ms) SELECT "boards".* FROM "boards" LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation []> ``` 進入`--sandbox`模式假裝創建資料 ``` /PPT   master  rails c --sandbox Running via Spring preloader in process 1614 Loading development environment in sandbox (Rails 5.2.4.3) Any modifications you make will be rolled back on exit ``` ``` 2.5.2 :001 > Board.create(title: 'Ruby') (0.1ms) SAVEPOINT active_record_1 Board Create (1.6ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Ruby"], ["created_at", "2020-07-23 06:30:02.969252"], ["updated_at", "2020-07-23 06:30:02.969252"]] (0.1ms) RELEASE SAVEPOINT active_record_1 => #<Board id: 1, title: "Ruby", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 06:30:02", updated_at: "2020-07-23 06:30:02"> ``` ## 追查類別的方法定義在哪一行 `.source_location` ``` 2.5.2 :002 > Board => Board(id: integer, title: string, intro: text, deleted_at: datetime, state: string, created_at: datetime, updated_at: datetime) 2.5.2 :003 > Board.method(:all) => #<Method: Board(id: integer, title: string, intro: text, deleted_at: datetime, state: string, created_at: datetime, updated_at: datetime).all> 2.5.2 :004 > Board.method(:all).source_location => ["/Users/tingtinghsu/.rvm/gems/ruby-2.5.2/gems/activerecord-5.2.4.3/lib/active_record/scoping/named.rb", 26] 2.5.2 :005 > 3.days.method(:ago).source_location => ["/Users/tingtinghsu/.rvm/gems/ruby-2.5.2/gems/activesupport-5.2.4.3/lib/active_support/duration.rb", 366] ``` # 列出boards看板列表 ## RESTful Routes [wiki](Representational State Transfer) 用同個標準命名路徑 視路徑為`resources` Eg. 看板可能的路徑path: ``` /boards /boards/2 /boards/2/edit ``` 1. rails g controller boards 2. 在controller新增methods ``` class BoardsController < ApplicationController def index @boards = Board.all #用實體變數裝起來傳給view end end ``` 3. 在view印出所有看板 ``` <h1>看板</h1> <%= @boards %> ``` ``` 看板 #<Board::ActiveRecord_Relation:0x00007fc91ded30f8> ``` 4. 用console c新增資料 ``` 2.5.2 :005 > Board.create(title: 'Ruby') (0.1ms) begin transaction Board Create (1.7ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "Ruby"], ["created_at", "2020-07-23 07:45:47.861782"], ["updated_at", "2020-07-23 07:45:47.861782"]] (1.3ms) commit transaction => #<Board id: 1, title: "Ruby", intro: nil, deleted_at: nil, state: "normal", created_at: "2020-07-23 07:45:47", updated_at: "2020-07-23 07:45:47"> 2.5.2 :006 > Board.create(title: 'PHP') (0.4ms) begin transaction Board Create (1.5ms) INSERT INTO "boards" ("title", "created_at", "updated_at") VALUES (?, ?, ?) [["title", "PHP"], ["created_at", "2020-07-23 07:46:35.574927"], ["updated_at", "2020-07-23 07:46:35.574927"]] (1.4ms) commit transaction => #<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"> ``` 5. 在view用`.each`印出所有的檔案 ``` <h1>看板</h1> <ul> <% @boards.each do |b| %> <li><%= b.title %></li> <% end %> </ul> ``` 網頁顯示 ``` 看板 Ruby PHP ``` # 新增看板 ## 建立連結 在view建立連結 ``` <a href="/boards/new">新增看板</a> ``` 改成`name_path` 1. 確保開發時路徑不會打錯 2. 路徑名稱更改時,連結自動對應 Q:主管說名稱從"boards"改為"cards"該怎麼做? A:進到routes.rb檔執行以下 ``` resources :boards, path: 'cards' ``` ``` <a href="<%= new_board_path %>">新增看板</a> ``` rails的寫法`link_to`, ``` <%= link_to '新增看板', new_board_path if false %> ``` ## 新增看板 new.html.erb, `method`用`post`,不會像`get`把參數(如密碼)直接放在網址上 ``` <h1>新增看板</h1> <form action="/boards" method="post"> <label for="title">看板名稱</label> <input type="text" name="title" id="title"><br> <label for="intro">說明</label> <textarea name="intro" id="intro"></textarea> <input type="submit" value="送出"> </form> ```` 在controller印出來傳進來的參數"create"看看 ``` def create render html: params end ``` 出現`InvalidAuthenticityToken`錯誤 ``` ActionController::InvalidAuthenticityToken in BoardsController#create ``` 如果沒有認證的Token,就沒有辦法寫入,資安上比較安全 params是從某一頁表單傳到另一列的參數 一個修正後的hash `params["title"]` `params[:title]`都可以拿到資料 先用爛方法在controller用render把答案印出來 ``` def create render html: params end ``` 頁面會顯示 http://localhost:3000/boards ``` {"authenticity_token"=>"KxKoyhFrzt59vnHbxTIRRtfX4eYzfRez5I7cY78DRN86H1nTdh+7joUBogmEIUPBf8HrnSWbMKOjeq7FAWU80A==", "title"=>"123", "intro"=>"456", "controller"=>"boards", "action"=>"create"} ``` 只抓出重要欄位並存入 ``` def create Board.create(title: params[:title], intro: params[:intro]) redirect_to "/" end ``` # 如何讓erb顯示出html的emmet? 設定 ![](https://i.imgur.com/8VVhibu.png) # 0724 第二天 ## form 可以長表單出來 有3種 1. `form_for`後面要接model `form_for(model)` ``` <%= form_for(Board.new) do |f| %> <% end %> ``` 2. `form_tag` 不需要model eg. 輸入身高體重、計算BMI(不需要用到資料庫) 3. form_for 二合一 - form_with(model:, url: ...) - form_with(url: ...) ## `form_for` `form_for(Board.new)` 會猜是要新增,決定路徑往哪裡去 (有的時候會猜錯,所以會在後面加上URL) ``` <%= form_for(ApplicationRecord.new) do |f| %> <% end %> ``` => 會出錯,抽象類別不能new出實體 ## 利用`form_for`建立欄位 ``` <%= form_for(Board.new) do |f| %> <%= f.label(:title), "看板名稱" %> <%= f.text_field(:title)%> <% end %> ``` 和昨天做的form有麼不同? 檢查網頁原始碼,原本的欄位 ``` <label for="title">看板名稱</label> <input type="text" id="title" name="title"><br /> <label for="intro">說明</label> <textarea id="intro" name="intro"></textarea> ``` ``` Parameters: {"authenticity_token"=>"35/KxPh1Xkg==", "title"=>"456", "intro"=>"789"} ``` 利用`form_for`做出來 ``` <label for="board_title">看板名稱</label> <input type="text" name="board[title]" id="board_title" /> <label for="board_intro">看板說明</label> <textarea name="board[intro]" id="board_intro"> ``` `form_for`根據`model的名字`和`欄位的名字`會包成一包`hash` ``` Parameters: {"utf8"=>"✓", "authenticity_token"=>"YnNwIUkCkvm=", "board"=>{"title"=>"123", "intro"=>"456"}, "commit"=>"送出"} ``` ## 改寫: 實體變數應該要放在controller controller ``` def new @board = Board.new end ``` view ``` <%= form_for(@board) do |f| %> <%= f.label :title, "看板名稱" %> <%= f.text_field :title %> <%= f.label :intro, "看板說明" %> <%= f.text_area :intro %> <%= f.submit "送出" %> <% end %> ``` ## `create method` ``` def new @board = Board.new #要先new實體變數不然表單的引數會是nil end def create # http沒有狀態,每次request都是全新的開始 # 兩次request 會產生兩個不一樣的實體 # 所以這裡的@board和new方法的@board是不同的 @board = Board.new(params[:board]) end ``` ## params的作用? params是一個方法,負責把傳過來的字串打包好成hash ``` #想像params方法定義在上層,作用看起來像這樣子: def params {:board => "aa"} end ``` ``` def create @board = Board.new(params[:board]) if @board.save # OK else # NG end # Board.create(title: params[:title], intro: params[:intro]) # redirect_to "/" end ``` 在index上用param的話,只能在console裡看到,頁面上不會顯示 ``` def index puts "---------------------" p params p params[:title] puts "---------------------" @boards = Board.all @boards = Board.all end ``` ``` <ActionController::Parameters {"controller"=>"boards", "action"=>"index"} permitted: false> nil ``` ## 新增看板成功寫入時,回到首頁 ``` def create @board = Board.new(params[:board]) if @board.save redirect_to boards_path else # NG end ``` # strong parameters strong parameters處理controller的 Forbidden Attributes Error問題 防止有心人士透過開發者工具塞參數進來 ``` def create # Strong Parameters clean_params = params.require(:board).permit(:title, :intro) @board = Board.new(clean_params) if @board.save redirect_to boards_path else # NG end end ``` 改寫成private方法 ``` private # Strong Parameters def board_params params.require(:board).permit(:title, :intro) end ``` ``` def create @board = Board.new(board_params) end ``` ## 思考controller controller幾乎不能重新使用,但是model可以 每個controller的method只能做一件事情 ``` def index @boards = Board.all render html: "hi" redirect_to root end ``` 錯誤訊息: `DoubleRenderError` ``` AbstractController::DoubleRenderError in BoardsController#index ``` controller的method預設會去尋找同名的view檔案,但一個method只能執行一個動作 eg. 如果又加了render -> 畫面顯示404頁面 ``` def index @boards = Board.all render file: "/public/404.html" # redirect_to '/' end ``` ## `flash[:notice]` controller: 新增成功時出現訊息 ``` if @board.save flash[:notice] = "新增成功!" redirect_to boards_path else # NG end ``` view ``` <h2><%= flash[:notice] %></h2> or <h2><%= notice %></h2> ``` 或是放在公板`application.html.erb`裡 flash太常使用了,所以也可以直接在controller提示: ``` if @board.save redirect_to boards_path, notice: "新增成功" else # NG end ``` ## 寫入失敗的條件 model的驗證條件 ``` class Board < ApplicationRecord validates :title, :intro, presence: true, length: {minimum: 2} # 舊式寫法 # validates_presence_of :title end ``` ``` ~/Documents/projects/PPT   master ●  rails c --sandbox Running via Spring preloader in process 6455 Loading development environment in sandbox (Rails 5.2.4.3) Any modifications you make will be rolled back on exit 2.5.2 :001 > b1 = Board.new => #<Board id: nil, title: nil, intro: nil, deleted_at: nil, state: "normal", created_at: nil, updated_at: nil> 2.5.2 :002 > b1.errors.any? => false 2.5.2 :003 > b1.save (0.2ms) SAVEPOINT active_record_1 (0.2ms) ROLLBACK TO SAVEPOINT active_record_1 => false 2.5.2 :004 > b1.errors.any? => true 2.5.2 :006 > b1.errors.full_messages => ["Title can't be blank", "Title is too short (minimum is 2 characters)", "Intro can't be blank", "Intro is too short (minimum is 2 characters)"] ``` 欄位出錯的時候,`form_for`會在錯誤欄位包一層東西, 如何用css呈現錯誤的欄位? [SASS](https://sass-lang.com/guide) 修改css ``` form { label { display: block; } .field_with_errors { input,textarea { border: 2px solid red; } } } ``` ## 把錯誤的原因印出來 在view跑迴圈 重要:不要在view裡面new出實體! ``` <% if @board.errors.any? %> <ul> <% @board.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> ``` # 建立各個看板的頁面 - index頁面跑迴圈加上超連結 ``` <ul> <% @boards.each do |board| %> <li><%= link_to board.title, board_path(board) %></li> <% end %> </ul> ``` - 建立show.html.erb - controller加上方法 試著把抓的param在show method印出來看看 ``` def show puts "-"* 50 p params puts "-"* 50 end ``` 點入任一個版面的連結 server會抓到參數 ``` Started GET "/boards/1" for 127.0.0.1 at 2020-07-24 15:23:20 +0800 Processing by BoardsController#show as HTML Parameters: {"id"=>"1"} -------------------------------------------------- <ActionController::Parameters {"controller"=>"boards", "action"=>"show", "id"=>"1"} permitted: false> -------------------------------------------------- ``` 透過`param[:id]`可以抓到數字 ## 透過參數撈資料的方法 ``` def show Board.where(id: params[:id]) => [1, 1] Board.find_by(id: params[:id]) # => 1 Board.find(params[:id]) # 同id有多筆時會出錯 # 以上3種都會翻譯成SQL # select * from boards where id = ? ``` ## `find` 和 `find_by`的差別 1. `find` 只能找`id` `find_by`可以接hash `Board.find_by(id: param[:id], intro: "aaa")` 2. 找不到時 `.find_by` 回傳`nil` ``` 2.5.2 :008 > Board.find_by(id: 1234) Board Load (0.5ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 1234], ["LIMIT", 1]] => nil ``` `.find`直接噴錯誤訊息 ``` 2.5.2 :011 > Board.find(1234) Board Load (0.3ms) SELECT "boards".* FROM "boards" WHERE "boards"."id" = ? LIMIT ? [["id", 1234], ["LIMIT", 1]] Traceback (most recent call last): 1: from (irb):11 ActiveRecord::RecordNotFound (Couldn't find Board with 'id'=1234) ``` 直接寫rescue ``` def show begin @board = Board.find(params[:id]) rescue render file: '/public/404.html', status: 404 end end ``` 讓controller method裡面更乾淨的方法: 所有controller只要找不到就回傳404 ``` class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound, with: :not_found private def not_found render file: '/public/404.html', status: 404 end end ``` 讓Font-end Controller 和 Back-end Controller分開 # Update view上建立新增連結 ``` <% @boards.each do |board| %> <li> <%= link_to board.title, board_path(board) %> <%= link_to '編輯', edit_board_path(board) %> </li> <% end %> ``` controller跟新增方法的流程類似 ``` def update @board = Board.find(params[:id]) if @board.update(board_params) redirect_to boards_path, notice: "更新成功" else render :edit end end ``` # Delete delete是特殊的動詞,使用時必須標注 `method: 'delete' `(預設`method: 'get'`) 重要:加上`data: {confirm: '確認刪除?'}`確認刪除 ``` <% @boards.each do |board| %> <li> <%= link_to board.title, board_path(board) %> <%= link_to '編輯', edit_board_path(board) %> <%= link_to '刪除', board_path(board), method: 'delete', data: {confirm: '確認刪除?'} %> </li> <% end %> ``` controller新增destroy方法 ``` def destroy @board = Board.find(params[:id]) @board.destroy redirect_to boards_path, notice: "刪除成功" end ``` ## 調整成假的刪除 改成`update` ``` def destroy @board = Board.find(params[:id]) #@board.destroy @board.update(deleted_at: Time.now) redirect_to boards_path, notice: "刪除成功" end ``` 列出沒有被刪掉的看板 ``` def index @boards = Board.where(deleted_at: nil) end ``` ## 自己寫一個destroy把它蓋掉 board.rb ``` class Board < ApplicationRecord validates :title, :intro, presence: true, length: {minimum: 2} # validates_presence_of :title def destroy update(deleted_at: Time.now) end end ``` # 整理重複的程式碼 1. 在某幾個action做事情 ``` before_action :find_board, only: [:show, :edit. :updat, :destroy] ``` ``` private def find_board @board = Board.find(params[:id]) end ``` 2. `_form.html.erb` 引入渲染的檔案 渲染的檔案盡量不要有區域變數 讓其他的地方帶實體變數進來,重複使用的好處 `_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 |f| %> <%= f.label :title, "看板名稱" %> <%= f.text_field :title %> <%= f.label :intro, "看板說明" %> <%= f.text_area :intro %> <%= f.submit "送出" %> <% end %> ``` new, show ``` <%= render 'form', board: @board %> ``` # 第三天 讓所有搜尋結果加上條件而不是只有`Board.all` Eg. 找product價錢大於50元 ``` Product.where(price > 50) => select * from products where price > 50 ``` 可以寫成 Product.cheap 或 Product.expensive 賦予這段程式碼意義 ``` class Product def self.expensive where("price > 100") end end ``` ## 什麼時候用類別方法? 什麼時候用實體方法? ``` Product.expensive => 使用類別方法 p = Product.new p.save => 使用實體方法 ``` ## 類別方法 找出存在的看板 ``` def self.avaliable where(deleted_at: nil) end ``` ## scope的寫法 一行程式碼可以解決的話就用`scope` 回呼函數 `callback function` 前面不要忘記加逗號`,` ``` scope :available, -> { where(deleted_at: nil) } ``` ## 預設`default_scope` 作為預設的過濾條件 ``` default_scope { where(deleted_at: nil) } ``` 抵銷預設scope [Rails API] (https://github.com/rubysherpas/paranoia) `Board.unscoped` # 使用`paranoia`套件 實作假刪除功能 ## 用法 [Rubygem](https://rubygems.org/gems/paranoia) 複製最新版的套件 ``` gem 'paranoia', '~> 2.4', '>= 2.4.2' ``` 路徑:Gemfile/# Reduces boot times through caching; required in config/boot.rb ## 安裝套件的兩種方式 1. Gemfile <- gem_name `$ bundle(install)` 第一個方法會去更新`Gemfile.lock檔`,所以速度比較慢 2. `$ install gem_name` # 新的model: Post 文章 - title:string 必填 - content: text - board_id: integer 必填 - board:references - board:belongs_to - 1.作用:產生board_id - 2. belongs_to board - state: string [draft/published/hidden] - deleted_at: datetime:nil:index - ip: string 必填 - serial: string:unique ``` $ rails g model Post title content:text board:belongs_to deleted_at:datetime:index ip_address serial:string:unique ``` 做錯了的話把`rails g` 改成`rails d`,就會刪掉原本建立的model # Post model 建立多對多關聯 一個看板有很多文章 ## has_many board.rb : `has_many :posts` ``` class Board < ApplicationRecord acts_as_paranoid has_many :posts validates :title, presence: true, length: { minimum: 2 } end ``` 進去rails c來看 ``` 2.5.2 :005 > b = Board.find(2) Board Load (0.4ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["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 :007 > b.posts Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy []> ``` 如果剛開始建立的欄位不是`board_id`,而是`b_id`,要自己手動加上`foreign_key` ``` has_many :posts, foreign_key: b_id ``` # 看板內建立新post ## belongs_to ``` class Post < ApplicationRecord belongs_to :board, optional: true end ``` 如果沒有指定`optional: true`就一定要`board_id`才能存入 有很多種方法: ``` [找到後指定id] px = Post.new(title: "aa") px.board_id = 9 px.save --- [跟上面的方法類似] Post.create(title: "aa", board_id: 9) --- [比較安全:先找到看板再新增文章然後存進去] bb = Board.find(9) px = Post.new(title: "aa") px.board = bb px.save --- [從belongs_to]的源頭往下寫 bb = Board.find(9) bb.posts.create(title: "aa") ``` 方法一 從post指定`board_id` ``` 2.5.2 :001 > p = Post.new => #<Post id: nil, title: nil, content: nil, board_id: nil, deleted_at: nil, ip_address: nil, serial: nil, created_at: nil, updated_at: nil> 2.5.2 :003 > p.board_id = 2 => 2 2.5.2 :004 > p.save (0.2ms) begin transaction Board Load (0.6ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] Post Create (2.9ms) INSERT INTO "posts" ("board_id", "created_at", "updated_at") VALUES (?, ?, ?) [["board_id", 2], ["created_at", "2020-07-30 05:58:46.270668"], ["updated_at", "2020-07-30 05:58:46.270668"]] (1.7ms) commit transaction => true ``` 方法二: 從board對應 因為belongs_to幫忙post產生了兩個方法 ``` class Post < ApplicationRecord belongs_to :board # .board # .board=[] end ``` ``` 2.5.2 :006 > b = Board.second Board Load (8.8ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL ORDER BY "boards"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]] => #<Board id: 3, title: "Python", intro: "123", deleted_at: nil, state: "normal", created_at: "2020-07-23 09:11:39", updated_at: "2020-07-24 08:31:13"> 2.5.2 :007 > p = Post.new => #<Post id: nil, title: nil, content: nil, board_id: nil, deleted_at: nil, ip_address: nil, serial: nil, created_at: nil, updated_at: nil> 2.5.2 :008 > p.board = b => #<Board id: 3, title: "Python", intro: "123", deleted_at: nil, state: "normal", created_at: "2020-07-23 09:11:39", updated_at: "2020-07-24 08:31:13"> 2.5.2 :009 > p.save (1.2ms) begin transaction Post Create (1.9ms) INSERT INTO "posts" ("board_id", "created_at", "updated_at") VALUES (?, ?, ?) [["board_id", 3], ["created_at", "2020-07-30 06:05:30.718942"], ["updated_at", "2020-07-30 06:05:30.718942"]] (1.2ms) commit transaction => true 2.5.2 :011 > b.posts.count (0.5ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."board_id" = ? [["board_id", 3]] => 1 ``` 方法三: 直接從看板的角度建立一篇文章 因為`has_many`幫我們作出了`create`和`build`方法 ``` b.posts.create(title: "aa") ``` ``` 2.5.2 :012 > b.posts.create(title: "從board的方式 create post") (0.2ms) begin transaction Post Create (1.2ms) INSERT INTO "posts" ("title", "board_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "從board的方式 create post"], ["board_id", 3], ["created_at", "2020-07-30 06:10:30.054054"], ["updated_at", "2020-07-30 06:10:30.054054"]] (1.2ms) commit transaction => #<Post id: 3, title: "從board的方式 create post", content: nil, board_id: 3, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 06:10:30", updated_at: "2020-07-30 06:10:30"> ``` ## has_one和 has_many的差別? 兩種方法長得有點不一樣 ``` Has Many bb.posts.create ... bb.posts.build / save Has One bb.create_post bb.build_post ``` Eg. ``` class Board < ApplicationRecord validates :title, :intro, presence: true, length: {minimum: 2} acts_as_paranoid has_many :posts has_one :post end ``` `has_one`的SQL語法只找一筆 ``` 2.5.2 :001 > b = Board.find 2 Board Load (0.5ms) SELECT "boards".* FROM "boards" WHERE "boards"."deleted_at" IS NULL AND "boards"."deleted_at" IS NULL AND "boards"."id" = ? LIMIT ? [["id", 2], ["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 :002 > b.posts Post Load (2.0ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Post id: 1, title: nil, content: nil, board_id: 2, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 05:58:46", updated_at: "2020-07-30 05:58:46">]> 2.5.2 :003 > b.post Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."board_id" = ? LIMIT ? [["board_id", 2], ["LIMIT", 1]] => #<Post id: 1, title: nil, content: nil, board_id: 2, deleted_at: nil, ip_address: nil, serial: nil, created_at: "2020-07-30 05:58:46", updated_at: "2020-07-30 05:58:46"> ``` # 從頁面上新增文章 不好的網址設計: ``` Rails.application.routes.draw do resources :boards do resources :posts end end ``` 進階的寫法:把文章包進看板裡,把不需要的功能抽掉 ``` Rails.application.routes.draw do resources :boards do resources :posts, only: [:index, :new, :create] end resources :posts, except: [:index, :new, :create] end ``` [官網](https://rails.ruby.tw/routing.html) shallow (淺層嵌套) ``` Rails.application.routes.draw do resources :boards do resources :posts, shallow: true end ``` ## 練習 - post controller - new頁面 - form_for ``` <%= form_with(model: @post) do |f| %> <%= f.label :title, "文章名稱" %> <%= f.text_field :title %> <%= f.label :content, "文章內容" %> <%= f.text_area :content %> <%= f.submit "送出" %> <% end %> ``` `form_with` 要塞model給它, 然後指定url(不然猜不到) new 完之後送到create,路徑是`board_posts_path` ``` <%= form_with(model: @post, url: board_posts_path) do |form| %> <%= form.submit %> ``` # form_for 和 form_with差異 [rails在建立表單的時候-form-for-跟-form-with-有什麼不同](https://medium.com/@anneju/rails%E5%9C%A8%E5%BB%BA%E7%AB%8B%E8%A1%A8%E5%96%AE%E7%9A%84%E6%99%82%E5%80%99-form-for-%E8%B7%9F-form-with-%E6%9C%89%E4%BB%80%E9%BA%BC%E4%B8%8D%E5%90%8C-ec45cebbbf92) form_with是用Ajax方式送資料,所以畫面上不會有動作 [送資料的方式:turbolinks](https://www.writershelf.com/article/rails-turbolinks%E2%84%A2-5-%E6%B7%B1%E5%BA%A6%E7%A0%94%E7%A9%B6?locale=zh-TW) 寫法的差異 ``` <%= form_with(model: @post, url: board_posts_path) do |form| %> <%= form_for(@post, url: board_posts_path) do |form| %> ``` # 0731 第四天 資料驗證 補充昨天說明:rails scafford在form_with也是先設定 `local: true` (先逃避turbolinks的問題) ## 刪除看板會發生什麼事? 回想資料庫的資料的相依性(PK, FK) [資料的association: dependent](https://guides.rubyonrails.org/association_basics.html#options-for-has-one-dependent) Eg. 如果設定 `dependent: :destroy` ``` has_many :posts, dependent: :destroy ``` SQL會先幫我們砍看板,然後跟著砍掉文章 但是因為我們已經裝了`paranoid`假刪除功能 刪了等於沒刪 (`dependent: :destroy`這句無效) ``` class Board < ApplicationRecord acts_as_paranoid has_many :posts, dependent: :destroy end ``` # 幫文章設計序號 ## 問題:如何產生8位數亂碼? ``` def serial_generator(n) # a-z / A-Z / 0-9 產生亂數 end puts serial_generator(8) ``` ## 思考 先把三組陣列蒐集起來 ``` list = [*'A'..'Z'] + [*'a'..'z'] + [*'1'..'9'] ``` ## 解法 解法一. 把陣列利用`sample`取出八個數字,再`join`成字串 ``` list.sample(8).join ``` 解法二. 如果不知道`sample`這個method,想像洗牌的方式, 先`shuffle`洗牌再取出前8張 ``` list.shuffle.first(8).join ``` ## 問題:8位數亂碼扣掉容易混淆的字元? ``` list = [*'A'..'Z'] + [*'a'..'z'] + [*'1'..'9'] pool = list - ['i','I', 'l', 'L', '1', '0', 'o', 'O'] pool.shuffle.first(8).join ``` 排列組合的機率,保證序號重複可能性很低! ![](https://i.imgur.com/ITFUHXb.png) ## 把驗證序號加入Post Model [Rails Guide - 2.11 uniqueness](https://guides.rubyonrails.org/active_record_validations.html#uniqueness) ``` class Post < ApplicationRecord validates :title, presence: true, length: {minimum: 2} validates :serial, presence: true, uniqueness: true belongs_to :board end ``` `uniqueness`也可以加上`scope` 官方手冊的例子 - 每一年都有一次聖誕節(名稱會重複) `scope: :year` - 但是同一年的節日名稱不會重複 `validates :name, uniqueness` ``` class Holiday < ApplicationRecord validates :name, uniqueness: { scope: :year, message: "should happen once per year" } end ``` ## 撰寫Post Model 裡的method 注意:不要寫在controller裡 [Callback回呼流程](https://railsbook.tw/chapters/19-model-validation-and-callback.html) rails model的存檔流程 ![](https://i.imgur.com/Vj9hF1M.png) 先用鴕鳥方式處理舊文章:`allow_nil: true` ``` class Post < ApplicationRecord validates :title, presence: true, length: {minimum: 2} # 資料庫已經有很多文章是nil,非uniqueness validates :serial, presence: true, uniqueness: true, allow_nil: true belongs_to :board before_create :create_serial #存檔前先建立序號 private def create_serial self.serial = serial_generator(10) # attr_reader的方法,省略小括弧 # self.serial=(serial_generator(10)) end def serial_generator(n) [*'a'..'z', *'A'..'Z', *0..9].sample(n).join end end ``` ## Rake 寫一個rake來更新還沒有序號的舊文章 結構 ``` namespace :db do desc "更新文章序號" task :update_post_serial do # puts "hi" end end ``` `Rake -T` 指令列出所有的task ``` ~/Documents/projects/PPT   master ●  rake -T rake about # List rake db:structure:dump # Dumps the database structure to db/... rake db:structure:load # Recreates the databases from the st... rake db:update_post_serial # 更新文章序號 ``` 把剛剛寫的`亂數產生序號`方法塞進去 挑出序號為nil的文章,`update post`的序號 ``` namespace :db do desc "更新文章序號" task :update_post_serial do Post.where(serial: nil).each do |post| post.update(serial: serial_generator(10)) end end private def serial_generator(n) [*'a'..'z', *'A'..'Z', *0..9].sample(n).join end end ``` ## 執行Rake `task :update_post_serial => :environment` 指定環境變數,不然會找不到`Post` ``` namespace :db do desc "更新文章序號" task :update_post_serial => :environment do puts "-----------------" puts " updating serial " puts "-----------------" Post.where(serial: nil).each do |post| post.update(serial: serial_generator(10)) print "." end puts "done!" end private def serial_generator(n) [*'a'..'z', *'A'..'Z', *0..9].sample(n).join end end ``` 印出提示字元,讓使用者知道已經做完 ```  ~/Documents/projects/PPT   master ●  rails db:update_post_serial ----------------- updating serial ----------------- ......done! ``` 注意:呼叫方法時要注意receiver是誰 (例如private method的話就不能明確指出receiver) 如果self沒寫出來(隱含的self),跟寫出來self的意義不一樣 ``` task :update_post_serial => :environment do Post.where(serial: nil).each do |post| post.update(serial: serial_generator(10)) end puts "done!" end ``` ## Ruby `Self` 和 JS `this`的差別 (待補充) ## Ruby查找檔案的過程 `Cat` -> `Animal` -> `Object` -> `BasicObject` 檔案查找到Basic還找不到的時候會再回來Cat問有沒有`method_missing` 如果有的話,印出來 找不到的話,呼叫`super`上層來做 ``` module Kernal def method_missing(...) # 錯誤訊息印在這裡 end end ``` 這就是Ruby效能比較不好的原因 ``` # 效能差 Board.find_by_id_title(1, "aaa") # 直接用hash找,效能好 Board.find_by(id: 1, title: "aaa") ``` # User 使用者登入登出功能 ``` model: User - accout:string - password:string - email:string - nickname:string - gender:string - state:string - deleted_at:datetime:index model: Profile - 簽名檔 - 名片檔 ``` Counter Cache: 用在上站次數。每次登入的時候先存入,增加效能 ## 建Model之前,先了解資料庫的正規化 讓每個資料表只存該存的資料就好 ### 第一正規化 (1NF) 每個欄位應該只有一筆資料 => 拆開來 刪除資料中的重複群組 至少要做到第一正規化,不然就失去了使用資料庫的意義 ### 第二正規化 (2NF) 1. 需要滿足 1NF 2. 去除「部份相依性(Partial Dependency)」 Eg.三個model - student model (一個學生有很多技能) - skill model (每個技能都有很多學生使用) - skillset model (把相依性拆出來) ### 第三正規化 (3NF) 1. 需要滿足 2NF 2. 移除「遞移相依」(Transitive Dependency) / 間接相依 - skills model 只留teacher id,不需要teacher的其他資訊 - 拆出 teacher model ## rails g model ``` rails g model User account:string:uniq password email:string:uniq nickname gender state deleted_at:datetime:index ``` 然後rails db:migrate ## user model 加上驗證 密碼加密 md5 -> 不安全 SHA-1 SHA-256 [Digest::SHA1](https://ruby-doc.org/stdlib-2.5.1/libdoc/digest/rdoc/Digest.html) rails c會載入常用的標準函式庫 require只會載入一次 irb ``` ~/Documents/projects/PPT   master ●  irb 2.5.2 :001 > require 'digest' => true 2.5.2 :002 > require 'digest' => false 2.5.2 :003 > require 'digest' => false ``` ## 撰寫加密method ``` # keyword argument 關鍵字引數 def self.login(options) if options[:account] && options[:password] find_by(account: options[:account], # 帳號: aa password: add_salt(options[:password])) # 密碼: 已經加密為xyz # else # return false end end private def encrypt_password # 註冊時加密 self.password = User.add_salt(self.password) end def self.add_salt(password) Digest::SHA1.hexdigest("x#{password}y") end ``` console c ``` 2.5.2 :001 > User.login(account: "cc", password: "123") User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users"."account" = ? AND "users"."password" = ? LIMIT ? [["account", "cc"], ["password", "9eb8e9439adfea0f1615e2dc268117a2a32cb3bf"], ["LIMIT", 1]] => nil ``` # user route 單數和複數會長出不一樣的路徑 ``` resources :users /users/2/edit #=> 不應該給使用者看到id resource :users /user/edit #=> 比較好的寫法 ``` ``` resources :users, only: [:new, :create] do # 只有新增時看到自己序號, index user列表是給後台管理用的 member do get :profile_member end collection do # 擴充網址時,collection沒有id get :profile_collection end end ``` 做出來路徑的差別 ``` profile_member_user GET /users/:id/profile_member(.:format) users#profile_member profile_collection_users GET /users/profile_collection(.:format) users#profile_collection ``` logout動詞用`delete` ``` resources :users, only: [:new, :create] do collection do get :profile get :login delete :logout end end ``` ## 跳脫CRUD制式的網址設計 不想用原本的八條路徑,更進階的寫法 Eg.把原本new的路徑改成sign_up,英文比較符合一般使用者註冊時的流程 ``` resources :users, only: [:create] do collection do get :sign_up get :edit patch :update get :sign_in post :login delete :sign_out end end ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully