over 1 year ago

接續 RailsFun_新手教學_day1 _HD

參考:Parsing HTML with Nokogiri

北醫:

北醫的首頁:http://www.tmuh.org.tw/
產科的首頁:http://www.tmuh.org.tw/team/team/053
A醫生的個人頁面:http://www.tmuh.org.tw/team/team/053/053/050216
B醫生的個人頁面:http://www.tmuh.org.tw/team/team/053/053/050117

這兩個要先require
require 'open-uri'
require 'nokogiri'
terminal:由產科的首頁,先抓下來兩個網址
html = open('http://www.tmuh.org.tw/team/team/053').read
doc = Nokogiri::HTML(html)
new_html = doc.css(".media-left a").map{|link| link['href']}
temp_ans = new_html.map{|url| url.match(/^htpp/) ? url : "http://www.tmuh.org.tw#{url}"}
=> ["http://www.tmuh.org.tw/team/team/053/053/050216", "http://www.tmuh.org.tw/team/team/053/053/050117"]
terminal:先試看看抓醫生的名字下來
doctors = []
temp_ans.each do |f|
html = open(f).read
doc = Nokogiri::HTML(html)
doctors << {
name: doc.xpath('//h2').text
}
end
doctors
[{:name=>"區慶建 "}, {:name=>"簡立維 "}]
doctors[0][:name]
=> "區慶建 "
terminal:先試看看抓學歷這個項目名稱下來
doc.css('.col-sm-8 > p > span > strong')[0].text
=> "學歷"
doc.xpath('//strong')[0].text
=> "學歷"
terminal:用css去找
doc.css('.media > .media-left > a > img')
terminal
irb(main):001:1> imgs.each do |img|
irb(main):002:1* puts img.attr('src')
irb(main):003:1> end; true
terminal:抓產科首頁的兩個醫生的照片
doc.css(".media-left a")
terminal:把 a tag 的href拿掉的兩種方法
doc.css(".media-left a").map{|link| link['href']}
p doc.xpath('//div[@class="heat"]/a').map { |link| link['href'] }
terminal:用wget 把檔案抓到指定的資料夾 wget -P 資料夾路徑 檔案網址
wget -P /Users/tienshunlo/workspace/broadway/app/assets/images http://www.tmuh.org.tw//UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg

抓A醫生的個人頁面

準備工作做好
require 'open-uri'
require 'nokogiri'
html = open('http://www.tmuh.org.tw/team/team/053/053/050117').read
doc = Nokogiri::HTML(html)
1. 把名字找出來
把名字找出來
name = doc.css('//h2').text.strip
2. 把所有跟學歷、經歷、證照相關的項目找出來
把所有跟學歷、經歷、證照相關的項目找出來
regex =  doc.css('.col-sm-8 > p').to_html
ans = []
ans = regex.scan(/>([^\n><]+)</).flatten.map{|i| i.gsub(/•/ , '').gsub(/&nbsp;/ , '').gsub(/\u00a0/, '').strip}.delete_if{|i|i.empty?}

解釋:
- 要用to_html,把Nokogiri::XML::NodeSet轉成html,不然沒辦法用scan
- 參考Nokogiri 問題求救
- Regex

把相關項目按學歷、經歷、證照分類
temp = []
ans.each do |str|
    case str
    when "學歷" , "經歷" , "專科證書名稱"
        temp << []
    else
        temp[-1] << str
    end
end
terminal:最後結果會變這樣
temp
=> [["臺北醫學大學醫學系"], ["臺北醫學大學附設醫院 超音波科主任", "臺北醫學大學副教授"], ["婦產科專科醫師證書", "超音波專業醫師證書", "台灣周產期專科醫師證書"], ["臺北醫學大學醫學系"], ["臺北醫學大學附設醫院 超音波科主任", "臺北醫學大學副教授"], ["婦產科專科醫師證書", "超音波專業醫師證書", "台灣周產期專科醫師證書"]]
3. 抓照片的URL:方法三應該最直接,直接找ID
terminal:#方法一
img = doc.css('img#MainPlaceHolder_imgPhoto')
img_url = img.attr('src')
puts img_url
terminal:#方法一 / 結果
=> [#<Nokogiri::XML::Element:0x3fcc0e534b24 name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e534a70 name="id" value="MainPlaceHolder_imgPhoto">, #<Nokogiri::XML::Attr:0x3fcc0e534a20 name="src" value="/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg">]>]

=> #<Nokogiri::XML::Attr:0x3fcc0e534a20 name="src" value="/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg">

=> /UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg
terminal:#方法二
img_url = doc.css('img').css('#MainPlaceHolder_imgPhoto').attr('src')
terminal:#方法二 / 結果
=> /UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg
terminal:#方法三
img = doc.css('#MainPlaceHolder_imgPhoto')
img_url = img.attr('src')
puts img_url
terminal:#方法三 / 結果
=> [#<Nokogiri::XML::Element:0x3fe86791b13c name="img" attributes=[#<Nokogiri::XML::Attr:0x3fe86791b0d8 name="id" value="MainPlaceHolder_imgPhoto">, #<Nokogiri::XML::Attr:0x3fe86791b0c4 name="src" value="/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg">]>]

=> #<Nokogiri::XML::Attr:0x3fcc0e534a20 name="src" value="/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg">

=> /UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg
terminal:#方法四 - 1
img = doc.css('img')
img.count
terminal:#方法四 - 1 / 結果
=> [#<Nokogiri::XML::Element:0x3fcc0e1a5b70 name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e1a5b0c name="class" value="tmuh_logo">, #<Nokogiri::XML::Attr:0x3fcc0e1a5af8 name="src" value="/images/logo.jpg">, #<Nokogiri::XML::Attr:0x3fcc0e1a5ae4 name="alt" value="回首頁">]>, #<Nokogiri::XML::Element:0x3fcc0e534b24 name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e534a70 name="id" value="MainPlaceHolder_imgPhoto">, #<Nokogiri::XML::Attr:0x3fcc0e534a20 name="src" value="/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg">]>, #<Nokogiri::XML::Element:0x3fcc0e5184ec name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e518474 name="src" value="/images/logo.jpg">, #<Nokogiri::XML::Attr:0x3fcc0e518460 name="style" value="width: 100%;">]>, #<Nokogiri::XML::Element:0x3fcc0e1716cc name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e171668 name="src" value="/images/badge.jpg">, #<Nokogiri::XML::Attr:0x3fcc0e171654 name="style" value="width: 100%;">]>]

=> 4
terminal:#方法四 -2
img[0]
img[1]
img[2]
img[3]
terminal:#方法四 -2 / 結果:img[1]才是要找的醫生照片
=> #<Nokogiri::XML::Element:0x3fcc0e1a5b70 name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e1a5b0c name="class" value="tmuh_logo">, #<Nokogiri::XML::Attr:0x3fcc0e1a5af8 name="src" value="/images/logo.jpg">, #<Nokogiri::XML::Attr:0x3fcc0e1a5ae4 name="alt" value="回首頁">]>

=> #<Nokogiri::XML::Element:0x3fcc0e534b24 name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e534a70 name="id" value="MainPlaceHolder_imgPhoto">, #<Nokogiri::XML::Attr:0x3fcc0e534a20 name="src" value="/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg">]>

=> #<Nokogiri::XML::Element:0x3fcc0e5184ec name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e518474 name="src" value="/images/logo.jpg">, #<Nokogiri::XML::Attr:0x3fcc0e518460 name="style" value="width: 100%;">]>

=> #<Nokogiri::XML::Element:0x3fcc0e1716cc name="img" attributes=[#<Nokogiri::XML::Attr:0x3fcc0e171668 name="src" value="/images/badge.jpg">, #<Nokogiri::XML::Attr:0x3fcc0e171654 name="style" value="width: 100%;">]>
terminal:#方法四 -3
img_url = img[1]['src']
terminal:#方法四 -3 / 結果
=> "/UploadFile/EMPLOYEE/20150908192641028_%e7%b0%a1%e7%ab%8b%e7%b6%ad_1.jpg"
4. 用wget抓下照片
terminal:記得要用``
profile_img = "http://www.tmuh.org.tw#{img_url}"
`wget --output-document="/Users/tienshunlo/workspace/broadway/app/assets/images/123.jpg" --directory-prefix="/Users/tienshunlo/workspace/broadway/app/assets/images"  #{profile_img}`
`wget -P /Users/tienshunlo/workspace/broadway/app/assets/images -O /Users/tienshunlo/workspace/broadway/app/assets/images/123.jpg  #{profile_img}`

解釋:
wget記得要用``包起來。
-P 是指定資料夾 ( --directory-prefix=)
-O 是改變檔案名稱 (--output-document=)
誰先寫無所謂,但一定要記得寫出完整的路徑,要被下載的路徑要擺最後。

5. 新增Play
terminal
@play = Play.new
        @play.title = name
        @play.play_img = File.open("app/assets/images/#{name}.jpg",'r')
        #File.new("/path/to/image.jpg","r")

        #File.open('/path/to/image.jpg', 'r'))

        @play.category = Category.first
        @play.director = temp[1].join("<p>")
        @play.description = temp[2].join("<p>")
        @play.save

解釋:
Paperclip Gem:一般來講,用Paperclip上傳檔案,會存三種大小的檔案在Project_name/public/system/裡面,
所以要用File.open的方法去找到用wget存下來的路徑。

6. 純.rb檔放置地方

參考:RailsFun.tw 新手教學 day3 HD:大概在53分鐘的地方
a. JC.rb檔案做在project_name/lib
b. 剛做好的時候,可以用ruby JC.rb
c. 然後到config/application.rbrequirerequire "JC" (不要有.rb)
d. require之後,可以用rails c試看看,先require "JC",然後再JC.run (我是設定self.run)
e. 還可以到view去使用 <%= JC.run %>,不過server記得要重開

JC.rb的寫法,放在lib folder,要先require
require 'open-uri'
require 'nokogiri'
class JC
    def self.run
        html = open('http://www.tmuh.org.tw/team/team/053/053/050216').read
        doc = Nokogiri::HTML(html)
        #名稱

        name = doc.css('//h2').text.strip     
        #圖檔

        img = doc.css('#MainPlaceHolder_imgPhoto')
        img_url = img.attr('src')
        profile_img = "http://www.tmuh.org.tw#{img_url}"
        `wget -P /Users/tienshunlo/workspace/broadway/app/assets/images -O /Users/tienshunlo/workspace/broadway/app/assets/images/#{name}.jpg  #{profile_img}`
        #學歷、經歷、專科證書名稱

        ans = []
        ans = doc.css('.col-sm-8 > p').to_html.scan(/>([^\n><]+)</).flatten.map{|i| i.gsub(/•/ , '').gsub(/&nbsp;/ , '').gsub(/\u00a0/, '').strip}.delete_if{|i|i.empty?}
        temp = []
        ans.each do |str|
          case str
          when "學歷" , "經歷" , "專科證書名稱"
            temp << []
          else
            temp[-1] << str
          end
        end
        #新增Play

        @play = Play.new
        @play.title = name
        @play.play_img = File.open("app/assets/images/#{name}.jpg",'r')
        @play.category = Category.first
        @play.director = temp[1].join("<p>")
        @play.description = temp[2].join("<p>")
        @play.save
    end
end
 
almost 2 years ago

參考資料:ROR TUTORIAL Chapter 1:11. First-time repository setup
參考資料:RailsFun.tw 新手教學_day2 HD

1.開始使用:

git init --bare prodject.git

打ls 之後,會看到一個project.git的資料夾,裡面會有
branches config description HEAD hooks info objects refs等資料夾

2.把遠端的東西抓下來

git clone

3.檢查狀態

git status
git add init 加到一個暫存的狀態
git commit -m "註解"
git push origin master

4.開始測試:弄一個project2

git clone /home/imroot/git_temp/project.git/

5.全部加到一個暫存的狀態

git add -A 全部加到一個暫存的狀態
git commit -m
git status 
git push

6.換到project 2git pull下來
7.衝突的解法

git add -A
git commit
git push   會出現rejected會被中斷掉

8.解法:git pull
會開始執行auto merge,只要不是同一行就會成功,同一行會失敗
會顯示哪一個檔案merge失敗
到檔案去修改,然後會出現both modified
還是衝突狀態

9.git add filename之後會變成modified,這樣就可以commit了

git commit
git push

10.到projct 2,再執行git pull,就可以了
11.git pull之後,如果沒有conflict,就會auto merge成功,就會進到commit的畫面
12.講branch:新增brance

git branch newbranch

13.小寫d刪除,還沒被merge

git -d branchname 
  1. 大寫D,強制刪除
    git -D branchname 
    
    15.換到新的branchname
    git checkout branchname
    
    16.兩個branches要merge時正確的做法 merge要先由branch去合併master 然後再由master去合併branch
    git checkout brancename
    git merge master
    auto merge會有conflict,要解conflict之後
    git add -A
    git commit -m 'fix'
    git checkout master
    git merge branchname
    
    17.git log 可以看過去的所有的commit git checkout commit_number

18.34:00 git總整理

git clone init --bare project.git

下面:用--bare創建裸庫,用ls,就可以在裸庫裡面看到一些資料夾了

mkdir git-temp
/git-temp$ git init --bare project.git
/git-temp/project.git$ ls
branches config description HEAD hooks info objects refs

下面:如果不是裸庫,要用ls -al才看得到.git.git裡面才有一些資料夾。

/git-temp$ git init project
/git-temp/project $ ls -al
.git

下面:第一個是遠端的。第二個是本地端的。

git clone ssh://non,tw/var/git/project.git
git clone /home/imroot/git-temp/project.git

下面:clone之後會有一個project資料夾(不是project.gt),也是要用ls - al才看得到.git

Cloning into 'project'
warning: you appear to have cloned an empty directory.
cd project
/project$ ls
/project$ ls -al
.git
/project$ git status
On branch master

Initial Commit

nothing to commit
 
almost 2 years ago

備註:

  1. 記得弄個新branch
    $ git checkout master
    $ git checkout -b following-users
    
  2. map怎麼寫

重點:

  1. hobbes.followers: 有哪些追隨者
  2. calvin.following: 追隨了哪些人
  3. 用Relationship model 來當中介可以簡化結構 $ rails generate model Relationship follower_id:integer followed_id:integer
  4. 為了效率,所以增添index
    Because we will be finding relationships by follower_id and by followed_id, we should add an index on each column for efficiency, as shown in Listing 12.1.

    db/migrate/[timestamp]_create_relationships.rb (Listing 12.1)
     class CreateRelationships < ActiveRecord::Migration
    def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id
    
      t.timestamps null: false
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
    end
    end
    

  5. add_index :relationships, [:follower_id, :followed_id], unique: true這行可以避免user重覆跟隨另一個user
    Listing 12.1 also includes a multiple-key index that enforces uniqueness on (follower_id, followed_id) pairs, so that a user can’t follow another user more than once. (Compare to the email uniqueness index from Listing 6.28 and the multiple-key index in Listing 11.1.) As we’ll see starting in Section 12.1.4, our user interface won’t allow this to happen, but adding a unique index arranges to raise an error if a user tries to create duplicate relationships anyway (for example, by using a command-line tool such as curl).

  6. 因為都是User,所以另外取名會比較好記。取名作follower跟followes,但其實都是連結回去User Class

    app/models/relationship.rb (Listing 12.3)
     class Relationship < ActiveRecord::Base
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
    end
    

    這邊是跟隨者has_many :active_relationships, class_name: "Relationship",foreign_key: "follower_id",dependent: :destroy

    app/models/user.rb(Listing 12.2)
     class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
    end
    

    這邊是跟隨誰:has_many :following, through: :active_relationships, source: :followed

    app/models/user.rb (Listing 12.8)
     class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
    has_many :following, through: :active_relationships, source: :followed
    end
    


    Rails would see “followeds” and use the singular “followed”, assembling a collection using the followed_id in the relationships table. But, as noted in Section 12.1.1, user.followeds is rather awkward, so we’ll write user.following instead.
    Naturally, Rails allows us to override the default, in this case using the source parameter (as shown in Listing 12.8), which explicitly tells Rails that the source of the following array is the set of followed ids.
    跟隨者:是active的關係
    跟隨誰:是passive的關係
    其實都是連到Relationship這個table,這樣取名(:active_relationships:passive_relationships)是為了比較好了解。要用class_name
    然後要去找到特定的foreign_key,active去找follower_id,passive去找followed_id

    app/models/user.rb (Listing 12.12)
     class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
    has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
    has_many :following, through: :active_relationships,  source: :followed
    has_many :followers, through: :passive_relationships, source: :follower
    end
    
  7. 跟隨誰,應該是用followed,不過英文語法不對,所以改成following,不過還是要讓rails知道要去哪裡找,所以要加上source: :followed

    app/models/user.rb (Listing 12.8)
     class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
    has_many :following, through: :active_relationships, source: :followed
    end
    

    Rails would see “followeds” and use the singular “followed”, assembling a collection using the followed_id in the relationships table. But, as noted in Section 12.1.1, user.followeds is rather awkward, so we’ll write user.following instead. Naturally, Rails allows us to override the default, in this case using the source parameter (as shown in Listing 12.8), which explicitly tells Rails that the source of the following array is the set of followed ids.

  8. 跟隨跟不跟隨

    app/models/user.rb (Listing 12.10)
     class User < ActiveRecord::Base
    def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
    end
    def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
    end
    def following?(other_user)
    following.include?(other_user)
    end
    end
    

    Follows a user.

    app/models/user.rb (Listing 12.10)
     class User < ActiveRecord::Base
    def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
    end
    end
    

    Unfollows a user.

    app/models/user.rb (Listing 12.10)
     class User < ActiveRecord::Base
    def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
    end
    end
    

    Returns true if the current user is following the other user.

    app/models/user.rb (Listing 12.10)
     class User < ActiveRecord::Base
    def following?(other_user)
    following.include?(other_user)
    end
    end
    
  9. user.following => 會利用follower_id這個foreign key,去找active_relationships的followed_id (用source告知)
    (會利用follower_id:跟隨者有幾個被跟隨者)
    Figure 12.7: A model of followed users through active relationships.


    user.followers => 會利用followed_id這個foreign key,去找passive_relationships的follower_id (用source告知)
    (會利用followed_id:被跟隨的這個人有幾個跟隨者)
    Figure 12.9: A model for user followers through passive relationships.

    app/models/user.rb (Listing 12.12)
     class User < ActiveRecord::Base
    has_many :microposts, dependent: :destroy
    has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
    has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
    has_many :following, through: :active_relationships,  source: :followed
    has_many :followers, through: :passive_relationships, source: :follower
    end
    

    其實follower_id跟followed_id都是跑回去User放在relationship裡面的foreign_key

    app/models/relationship.rb (Listing 12.3)
     class Relationship < ActiveRecord::Base
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
    end
    

  10. 12.2.1 Sample following data 可以先用db/seeds.rb來互相跟隨

  11. 12.2.2 Stats and a follow form

    config/routes.rb(Listing 12.15)
     Rails.application.routes.draw do
    resources :users do
    member do
      get :following, :followers
    end
    end
    resources :account_activations, only: [:edit]
    resources :password_resets,     only: [:new, :create, :edit, :update]
    resources :microposts,          only: [:create, :destroy]
    end
    

    /users/1/following/users/1/followers
    You might suspect that the URLs for following and followers will look like /users/1/following and /users/1/followers, and that is exactly what the code in Listing 12.15 arranges. Since both pages will be showing data, the proper HTTP verb is a GET request, so we use the get method to arrange for the URLs to respond appropriately. Meanwhile, the member method arranges for the routes to respond to URLs containing the user id. The other possibility, collection, works without the id, so that
    /users/tigers

    resources :users do
    collection do
    get :tigers
    end
    end
    

    would respond to the URL /users/tigers (presumably to display all the tigers in our application).

  12. 用partial來做view

    app/views/shared/_stats.html.erb(Listing 12.16)
     <% @user ||= current_user %>
    <div class="stats">
    <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
    </a>
    <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
    </a>
    </div>
    
  13. count可以在資料庫裡面直接計算數字出來,比較有效率
    As in that case, Rails calculates the count directly in the database for efficiency.

  14. partial的使用方法:<%= render 'shared/stats' %>

    app/views/static_pages/home.html.erb(Listing 12.17)
     <% if logged_in? %>
    <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="stats">
        <%= render 'shared/stats' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
    </div>
    <% else %>
    .
    .
    .
    <% end %>
    
  15. _follow_form.html.erb這個partial,用render執行另外兩個partial:_follow.html.erb_unfollow.html.erb

    12.19: A partial for a follow/unfollow form.)
     <% unless current_user?(@user) %>
    <div id="follow_form">
    <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
    <% else %>
    <%= render 'follow' %>
    <% end %>
    </div>
    <% end %>
    

    跟隨的partial,要記得hidden_field_tag:followed_idcreate action
    這邊是用build
    其實只是個button(還不能作用,因為還沒有寫好controller,要到12.2.4才行)

    12.21: A form for following a user.)
     <%= form_for(current_user.active_relationships.build) do |f| %>
    <div><%= hidden_field_tag :followed_id, @user.id %></div>
    <%= f.submit "Follow", class: "btn btn-primary" %>
    <% end %>
    

    解除跟隨的partial,這邊是delete,其實只是個button(還不能作用,因為還沒有寫好controller,要到12.2.4才行)

    12.22: A form for unfollowing a user.)
     <%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
    <% end %>
    
  16. Listing12.16的stats parital裡面的<%= following_user_path(@user) %><%= followers_user_path(@user) %>必須要靠Listing12.15裡面的member去寫出來,可是Controller跟View都還沒寫好,就要看Listing 12.25跟Listing 12.26

    app/views/shared/_stats.html.erb(Listing 12.16)
     <% @user ||= current_user %>
    <div class="stats">
    <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
    </a>
    <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
    </a>
    </div>
    
    config/routes.rb(Listing 12.15)
     Rails.application.routes.draw do
    resources :users do
    member do
      get :following, :followers
    end
    end
    end
    

    註:member寫好會變這樣,網址會是:/users/1/following/users/1/followers但要記得回去Controller寫Action/Method = followingfollowers
    HTTP request URL Action Named route
    GET /users/1/following following following_user_path(1)
    GET /users/1/followers followers followers_user_path(1)

    app/controllers/users_controller.rb(Listing 12.25)
     class UsersController < ApplicationController
    before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
    def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
    end
    def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
    end
    private
    end
    
    app/views/users/show_follow.html.erb(Listing 12.26)
     <% provide(:title, @title) %>
    <div class="row">
    <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
    </aside>
    <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
    </div>
    </div>
    
  17. 12.2.4:開始讓button可以用
    $ rails generate controller Relationships
    幫用戶A在用戶B的頁面,看下follow button(12.21)之後,會將:followed_id送到Relationships Controllercreate method去 (12.32),執行user = User.find(params[:followed_id]),接著執行current_user.follow(user),會跑回去12.10的follow methodactive_relationships.create(followed_id: other_user.id),在Relationships Model的table裡面新增一列

    app/views/users/_follow.html.erb(Listing 12.21)
     <%= form_for(current_user.active_relationships.build) do |f| %>
    <div><%= hidden_field_tag :followed_id, @user.id %></div>
    <%= f.submit "Follow", class: "btn btn-primary" %>
    <% end %>
    
    app/views/users/_unfollow.html.erb(Listing 12.22)
     <%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
    <% end %>
    
    app/models/user.rb(Listing 12.10)
    class User < ActiveRecord::Base
    def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
    end
    def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
    end
    def following?(other_user)
    following.include?(other_user)
    end
    end
    
    app/controllers/relationships_controller.rb(Listing 12.32)
     class RelationshipsController < ApplicationController
    before_action :logged_in_user
    def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
    end
    def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
    end
    end
    

  18. 開始用ajax,只要在view的地方加remote: true,然後在controller也要加東西
    就好 12.2.5 A working follow button with Ajax

    app/views/users/_follow.html.erb (Listing 12.33)
     <%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
    <div><%= hidden_field_tag :followed_id, @user.id %></div>
    <%= f.submit "Follow", class: "btn btn-primary" %>
    <% end %>
    
    app/views/users/_unfollow.html.erb (Listing 12.34)
     <%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
    <% end %>
    
    app/controllers/relationships_controller.rb (Listing 12.35)
     class RelationshipsController < ApplicationController
    before_action :logged_in_user
    def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
    end
    def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
    end
    end
    

    這樣的寫法,即使是JavaScript 被停用了還是可以使用,不過還是要加點東西

    format.html { redirect_to @user }
    format.js
    
    config/application.rb(Listing 12.3)
    module SampleApp
    class Application < Rails::Application
    # Include the authenticity token in remote forms.
    
    config.action_view.embed_authenticity_token_in_remote_forms = true
    end
    end
    

    而且還要去把這兩個檔案寫好:create.js.erbdestroy.js.erb

    app/views/relationships/create.js.erb (Listing 12.37)
     $("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
    $("#followers").html('<%= @user.followers.count %>');
    
    app/views/relationships/destroy.js.erb (Listing 12.38)
     $("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
    $("#followers").html('<%= @user.followers.count %>');
    
  19. map怎麼用?
    Micropost.where("user_id = ?", id)
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    [1, 2, 3, 4].map { |i| i.to_s }
    [1, 2, 3, 4].map(&:to_s)
    User.first.following.map(&:id)
    User.first.following_ids
    User.first.following_ids.join(', ')
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    有5000 users就爆了

  20. subselect 子查詢
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
    following_ids: following_ids, user_id: id)

    following_ids = "SELECT followed_id FROM relationships
    WHERE follower_id = :user_id"

    大概是這個樣子:
    SELECT * FROM microposts
    WHERE user_id IN (SELECT followed_id FROM relationships
    WHERE follower_id = 1)
    OR user_id = 1

    app/models/user.rb(Listing 12.46)
    class User < ActiveRecord::Base
    def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE  follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
    end
    end
    
 
almost 2 years ago
  1. 每次都要新增一個branch
  2. 怎麼使用Amazon.com’s Simple Storage Service (S3)
    $ git checkout master
    $ git checkout -b user-microposts
    
  3. user:references這樣寫可以直接產生關聯,然後會有user_id (belongs_to :user會自動產生)

    $ rails generate model Micropost content:text user:references
    

    The generate command produces a migration to create a microposts table in the database (Listing 11.1); compare it to the analogous migration for the users table from Listing 6.2. The biggest difference is the use of references, which automatically adds a user_id column (along with an index and a foreign key reference)3 for use in the user/micropost association.

  4. 把micropost叫出來的時候,要按特定的user,還要有特定的順序,所以:user_id:create_at都要記得上index
    Because we expect to retrieve all the microposts associated with a given user id in reverse order of creation, Listing 11.1 adds an index (Box 6.2) on the user_id and created_at columns:

    add_index :microposts, [:user_id, :created_at]
    

    By including both the user_id and created_at columns as an array, we arrange for Rails to create a multiple key index, which means that Active Record uses both keys at the same time.

  5. 要記得rake db:migrate,資料庫才會被更新。
    With the migration in Listing 11.1, we can update the database as usual:

    $ bundle exec rake db:migrate
    
  6. assert @micropost.valid?答案是valid,所以assert會是true (因為都還沒有設任何的validation,要到micropost.rb去設validates :user_id, presence: true)

    test "should be valid" do
    assert @micropost.valid?
    end
    
  7. 'assert_not @micropost.valid?' 答案是valid,所以assert_not會是false (因為都還沒有設任何的validation,要到micropost.rb去設validates :user_id, presence: true)

    test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
    end
    
  8. assert_not @micropost.valid?也會是valid,雖然@micropost.content是空白的,但因為沒有validation,所以還是valid。

    test "content should be present" do
    @micropost.content = "   "
    assert_not @micropost.valid?
    end
    
  9. assert_not @micropost.valid?也是valid,因為還沒有設定少於140個字母。

    test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
    end
    
  10. 要這樣設才有會讓7跟8是not valid,然後才會變true:validates :content, presence: true, length: { maximum: 140 }

  11. 第二個寫法才是正確的,可以順便產生user_id

    Micropost.create
    Micropost.create!
    Micropost.new
    
    user.microposts.create
    user.microposts.create!
    user.microposts.build
    

    These latter methods constitute the idiomatically correct way to make a micropost, namely, through its association with a user. When a new micropost is made in this way, its user_id is automatically set to the right value.

  12. 這邊又要去想一下newbuild的差別
    (As with new, build returns an object in memory but doesn’t modify the database.)

  13. default_scope可以將資料從資料庫按照我們所需要的順序叫出來
    We’ll get the test to pass using a Rails method called default_scope, which among other things can be used to set the default order in which elements are retrieved from the database

  14. 'order'是default_scope的一個argument
    To enforce a particular order, we’ll include the order argument in default_scope, which lets us order by the created_at column as follows:order(:created_at)

  15. 這樣就可以由新到舊的。
    default_scope -> { order(created_at: :desc) }
    Listing 11.16 introduces the “stabby lambda” syntax for an object called a Proc (procedure) or lambda, which is an anonymous function (a function created without a name). The stabby lambda -> takes in a block (Section 4.3.2) and returns a Proc, which can then be evaluated with the call method. We can see how it works at the console:
    Proc(procedure)跟lambda是匿名函數(不需要取名字),然後可以用call 去呼叫。

    >> -> { puts "foo" }
    => #<Proc:0x007fab938d0108@(irb):1 (lambda)>
    >> -> { puts "foo" }.call
    foo
    => nil
    
  16. dependent: :destroy: user被刪除的時候,user的micropost也要被刪除,所以是要放在user.rb
    has_many :microposts, dependent: :destroy

  17. render @users這樣的寫法,rails會自己去找_user.html.erb的partial。所以我們可以用render @microposts去找 _micropost.html.erb 的partial

    Section 9.3.5
    <ul class="users">
    <%= render @users %>
    </ul>
    

    automatically renders each of the users in the @users variable using the _user.html.erb partial. We’ll define an analogous _micropost.html.erb partial so that we can use the same technique on a collection of microposts as follows:

    <ol class="microposts">
    <%= render @microposts %>
    </ol>
    
    app/views/microposts/_micropost.html.erb (Listing 11.21)
    <li id="micropost-<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    </span>
    </li>
    
  18. <%= will_paginate @microposts %>will_paginate如果是放在User Controller,會自動去找@user這個變數,如果是放在User Controller,但是又希望他去找其他的變數,如@microposts,那就要把變數寫在後面。如<%= will_paginate @microposts %>。要記得定義@microposts,@microposts = @user.microposts.paginate(page: params[:page])
    As before, we’ll use the will_paginate method:<%= will_paginate @microposts %>
    If you compare this with the analogous line on the user index page, Listing 9.41, you’ll see that before we had just<%= will_paginate %>
    This worked because, in the context of the Users controller, will_paginate assumes the existence of an instance variable called @users (which, as we saw in Section 9.3.3, should be of class ActiveRecord::Relation). In the present case, since we are still in the Users controller but want to paginate microposts instead, we’ll pass an explicit @microposts variable to will_paginate. Of course, this means that we will have to define such a variable in the user show action (Listing 11.22).

    (Listing 11.22)
     class UsersController < ApplicationController
    .
    .
    .
    def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
    end
    .
    .
    .
    end
    
  19. count的好處:不會把所有資料從資料庫叫出來,然後再去計算長度,這樣太沒效率。'count'會直接在資料庫裡面先算好數量之後。
    更進階的可以使用counter cache
    As with paginate, we can use the count method through the association. In particular, count does not pull all the microposts out of the database and then call length on the resulting array, as this would become inefficient as the number of microposts grew. Instead, it performs the calculation directly in the database, asking the database to count the microposts with the given user_id (an operation for which all databases are highly optimized). (In the unlikely event that finding the count is still a bottleneck in your application, you can make it even faster using a counter cache.)

  20. resources :microposts, only: [:create, :destroy] : 不需要new跟dit,只要create跟destroy因為大部份的動作都要在Profile跟Home執行。
    the interface to the Microposts resource will run principally through the Profile and Home pages, so we won’t need actions like new or edit in the Microposts controller; we’ll need only create and destroy.

  21. logged_in_user這個filter本來是放在User Controller裡面,可是因為現在Microposts Controller也要用,所以把他拿到Application controller裡面去,所有的Controller都可以用了!
    Writing the application code needed to get the tests in Listing 11.30 to pass requires a little refactoring first. Recall from Section 9.2.1 that we enforced the login requirement using a before filter that called the logged_in_user method (Listing 9.12). At the time, we needed that method only in the Users controller, but now we find that we need it in the Microposts controller as well, so we’ll move it into the Application controller, which is the base class of all controllers (Section 4.4.4). The result appears in Listing 11.31.

    app/controllers/application_controller.rb
    class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    include SessionsHelper
    private
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
    end
    
  22. 如果登入,就顯示app/views/shared/_user_info.html.erbapp/views/shared/_micropost_form.html.erb

    app/views/static_pages/home.html.erb
     <% if logged_in? %>
    <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    </div>
    <% else %>
    <% end %>
    
    app/views/shared/_micropost_form.html.erb
     <%= form_for(@micropost) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
    </div>
    <%= f.submit "Post", class: "btn btn-primary" %>
    <% end %>
    
  23. _micropost_form.html.erb這個partial是在home.html.erb裡面,所以裡面的@micropost要在static_pages_controller.rb裡面定義,不過這個變數@micropost還是會送到Micropost Controller的create method去,所以Create那邊要接好micropoat_params

    app/controllers/static_pages_controller.rb
     class StaticPagesController < ApplicationController
    def home
    @micropost = current_user.microposts.build if logged_in?
    end
    end
    
    app/controllers/microposts_controller.rb (Listing 11.34)
    class MicropostsController < ApplicationController
    before_action :logged_in_user, only: [:create, :destroy]
    def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
    end
    private
    def micropost_params
      params.require(:micropost).permit(:content)
    end
    end
    
  24. 每個user都要有feed,所以會把feed method放在User Controller裡。Micropost.where("user_id = ?", id)
    我的理解:在User的Profile裡,如果要叫出來一個User所有的microposts的話,因為有關聯的關係,所以可以打成@user.microposts,不過為了增加易讀性,所以要把它包裝在feed method裡面,就可以改寫成@user.feed。
    Since each user should have a feed, we are led naturally to a feed method in the User model, which will initially just select all the microposts belonging to the current user. We’ll accomplish this using the where method on the Micropost model (seen briefly before in Section 10.5), as shown in Listing 11.44.10

    app/models/user.rb (Listing 11.44)
     class User < ActiveRecord::Base
    def feed
    Micropost.where("user_id = ?", id)
    end
    end
    

    Alert readers might note at this point that the code in Listing 11.44 is essentially equivalent to writing

    def feed
    microposts
    end
    

    We’ve used the code in Listing 11.44 instead because it generalizes much more naturally to the full status feed needed in Chapter 12.

  25. ("user_id = ?", id)這樣的寫法是為了防止SQL injection。
    ensures that id is properly escaped before being included in the underlying SQL query, thereby avoiding a serious security hole called SQL injection. The id attribute here is just an integer (i.e., self.id, the unique ID of the user), so there is no danger of SQL injection in this case, but always escaping variables injected into SQL statements is a good habit to cultivate.

  26. 接下來要開始用partial

  27. 在首頁的地方,<%= render 'shared/feed' %> 將顯示_feed.html.erb這個partial。

    app/views/static_pages/home.html.erb (Listing 11.47)
     <% if logged_in? %>
    <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
    </div>
    <% else %>
    .
    .
    .
    <% end %>
    
  28. _feed.html.erb這個partial,又會顯示@feed_items這個變數,因為這個變數的class是Micropost,所以又會自動去找_micropost.html.erb這個partial。

    app/views/shared/_feed.html.erb (Listing 11.46)
     <% if @feed_items.any? %>
    <ol class="microposts">
    <%= render @feed_items %>
    </ol>
    <%= will_paginate @feed_items %>
    <% end %>
    

    Here Rails knows to call the micropost partial because each element of @feed_items has class Micropost. This causes Rails to look for a partial with the corresponding name in the views directory of the given resource:app/views/microposts/_micropost.html.erb

    app/views/microposts/_micropost.html.erb(Listing 11.21)
    <li id="micropost-<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    </span>
    </li>
    
  29. 如果送出micropost失敗的話,會break。如果輸入一個空的rray好像也沒用。
    At this point, creating a new micropost works as expected, as seen in Figure 11.15. There is one subtlety, though: on failed micropost submission, the Home page expects an @feed_items instance variable, so failed submissions currently break. The easiest solution is to suppress the feed entirely by assigning it an empty array, as shown in Listing 11.48. (Unfortunately, returning a paginated feed doesn’t work in this case. Implement it and click on a pagination link to see why.)

    app/controllers/microposts_controller.rb(Listing 11.48)
    class MicropostsController < ApplicationController
    before_action :logged_in_user, only: [:create, :destroy]
    def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
    end
    def destroy
    end
    private
    def micropost_params
      params.require(:micropost).permit(:content)
    end
    end
    
  30. <%= render @feed_items %><%= render @microposts %>,這邊的@變數裡面應該都有複數的東西,可是為什麼不必用到each?
    原來在Ch9的地方有講到:
    9.3.5 Partial refactoring

    app/views/users/index.html.erb(Listing 9.46)
    <% provide(:title, 'All users') %>
    <h1>All users</h1>
    <%= will_paginate %>
    <ul class="users">
    <% @users.each do |user| %>
    <%= render user %>
    <% end %>
    </ul>
    <%= will_paginate %>
    
    app/views/users/index.html.erb(Listing 9.48)
     <% provide(:title, 'All users') %>
    <h1>All users</h1>
    <%= will_paginate %>
    <ul class="users">
    <%= render @users %>
    </ul>
    <%= will_paginate %>
    

    Here Rails infers that @users is a list of User objects; moreover, when called with a collection of users, Rails automatically iterates through them and renders each one with the _user.html.erb partial. The result is the impressively compact code in Listing 9.48.
    雖然Rails會把@user當作User的列表,一個集合,但是在patial的情況下,他會一個一個顯示出來。

  31. 點了delete之後,micropost的id會被送到Micropost Controllerdestroy method去。
    因為有設before_action的關係,他會先去執行correct_user method
    @micropost = current_user.microposts.find_by(id: params[:id])
    這邊的id: params[:id]micropost的id
    we’ll find the micropost through the association, which will automatically fail if a user tries to delete another user’s micropost. We’ll put the resulting find inside a correct_user before filter, which checks that the current user actually has a micropost with the given id. The result appears in Listing 11.50.
    假設current_usermicropost中找不到這個id,會return nil(不會跳出錯誤訊息)
    (find會有錯誤訊息,find_by會return nil)
    find vs find_by vs where
    If no record is found, returns nil.

    app/views/microposts/_micropost.html.erb (Listing 11.49)
    <li id="<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
    </span>
    </li>
    
    app/controllers/microposts_controller.rb (Listing 11.50)
    class MicropostsController < ApplicationController
    before_action :logged_in_user, only: [:create, :destroy]
    before_action :correct_user,   only: :destroy
    .
    .
    .
    def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
    end
    private
    def micropost_params
      params.require(:micropost).permit(:content)
    end
    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
    end
    
  32. request.referrer || root_url
    This uses the request.referrer method,11 which is closely related to the request.url variable used in friendly forwarding (Section 9.2.3), and is just the previous URL (in this case, the Home page).12 This is convenient because microposts appear on both the Home page and on the user’s profile page, so by using request.referrer we arrange to redirect back to the page issuing the delete request in both cases. If the referring URL is nil (as is the case inside some tests), Listing 11.50 sets the root_url as the default using the || operator. (Compare to the default options defined in Listing 8.50.)

  33. 開始做上傳照片

  34. Micropost Model會需要用到下面的gem,把他們都加到Gemfile

    • carrierwave gem: 上傳照片
    • mini_magick gem:image resizing (Section 11.4.3)
    • fog gem: image upload in production (Section 11.4.4)
  35. $ rails generate uploader Picture:產生image uploader 圖片上傳器。
    CarrierWave adds a Rails generator for creating an image uploader, which we’ll use to make an uploader for an image called picture。

  36. 要用CarrierWave上傳照片很簡單:在generate之後,記得把名字(這邊是Picture)放到相對應的Model(這邊是Microposts)去就好了,記得要設成String
    Images uploaded with CarrierWave should be associated with a corresponding attribute in an Active Record model, which simply contains the name of the image file in a string field. The resulting augmented data model for microposts appears in Figure 11.19.
    To add the required picture attribute to the Micropost model, we generate a migration and migrate the development database:

    $ rails generate migration add_picture_to_microposts picture:string
    $ bundle exec rake db:migrate
    
  37. 然後要加上這行:mount_uploader :picture, PictureUploader,才有辦法把image跟model連接起來。
    The way to tell CarrierWave to associate the image with a model is to use the mount_uploader method, which takes as arguments a symbol representing the attribute and the class name of the generated uploader:
    (Here PictureUploader is defined in the file picture_uploader.rb, which we’ll start editing in Section 11.4.2, but for now the generated default is fine.) Adding the uploader to the Micropost model gives the code shown in Listing 11.56.

    app/models/micropost.rb (Listing 11.56)
     class Micropost < ActiveRecord::Base
    belongs_to :user
    default_scope -> { order(created_at: :desc) }
    mount_uploader :picture, PictureUploader
    validates :user_id, presence: true
    validates :content, presence: true, length: { maximum: 140 }
    end
    
  38. html: { multipart: true } :in the arguments to form_for, which is necessary for file uploads.
    <%= f.file_field :picture %>: To include the uploader on the Home page

    app/views/shared/_micropost_form.html.erb (Listing 11.57)
     <%= form_for(@micropost, html: { multipart: true }) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
    </div>
    <%= f.submit "Post", class: "btn btn-primary" %>
    <span class="picture">
    <%= f.file_field :picture %>
    </span>
    <% end %>
    
  39. image_tag helper可以叫出來照片。
    picture? boolean method 是CarrierWave跟據attribute的名字自動產生的method。
    Once the image has been uploaded, we can render it using the image_tag helper in the micropost partial, as shown in Listing 11.59. Notice the use of the picture? boolean method to prevent displaying an image tag when there isn’t an image. This method is created automatically by CarrierWave based on the name of the image attribute. The result of making a successful submission by hand appears in Figure 11.20. Writing an automated test for image upload is left as an exercise (Section 11.6).

    app/views/microposts/_micropost.html.erb (Listing 11.59)
     <li id="micropost-<%= micropost.id %>">
    <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
    <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
    <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
    </span>
    <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
    </span>
    </li>
    
  40. 開始限制上傳的照片的格式跟尺寸,在伺服器端跟瀏覽器端都需要做設定

  41. 伺服器端:限制上傳檔案的類型:picture_uploader.rb

    app/uploaders/picture_uploader.rb (Listing 11.60)
    class PictureUploader < CarrierWave::Uploader::Base
    storage :file
    # Override the directory where uploaded files will be stored.
    
    # This is a sensible default for uploaders that are meant to be mounted:
    
    def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
    end
    # Add a white list of extensions which are allowed to be uploaded.
    
    def extension_white_list
    %w(jpg jpeg gif png)
    end
    end
    
  42. 伺服器端:尺寸:Micropost model,size validation不是內建的,所以要自己寫

    app/models/micropost.rb (Listing 11.61)
     class Micropost < ActiveRecord::Base
    belongs_to :user
    default_scope -> { order(created_at: :desc) }
    mount_uploader :picture, PictureUploader
    validates :user_id, presence: true
    validates :content, presence: true, length: { maximum: 140 }
    validate  :picture_size
    private
    # Validates the size of an uploaded picture.
    
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
    end
    end
    
  43. 用戶端:有兩個
    類型:<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
    尺寸:

    $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
    alert('Maximum file size is 5MB. Please choose a smaller file.');
    }
    });
    
  44. 把Query放在最下面

    app/views/shared/_micropost_form.html.erb (Listing 11.62)
     <%= form_for(@micropost, html: { multipart: true }) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
    </div>
    <%= f.submit "Post", class: "btn btn-primary" %>
    <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
    </span>
    <% end %>
    <script type="text/javascript">
    $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert('Maximum file size is 5MB. Please choose a smaller file.');
    }
    });
    </script>
    
  45. ImageMagick: 變更上傳圖檔的尺寸

  46. MiniMagickCarrierWavemodule,是用來操控ImageMagick的介面

  47. 做一下比較:
    MiniMagick documentation
    這邊的MyUploader後面是CarrierWave::Uploader::Base
    這邊是不能超過200X200

    class MyUploader < CarrierWave::Uploader::Base
    include CarrierWave::MiniMagick
    process :resize_to_fit => [200, 200]
    end
    

    CarrierWave documentation on MiniMagick
    這邊的AvatarUploader的後面也是CarrierWave::Uploader::Base
    這邊是要放大到200X200

    class AvatarUploader < CarrierWave::Uploader::Base
    include CarrierWave::MiniMagick
    process resize_to_fill: [200, 200]
    end
    

    這邊的存檔的地方是在本地端的資料夾

    app/uploaders/picture_uploader.rb (Listing 11.63)
     class PictureUploader < CarrierWave::Uploader::Base
    include CarrierWave::MiniMagick
    process resize_to_limit: [400, 400]
    storage :file
    # Override the directory where uploaded files will be stored.
    
    # This is a sensible default for uploaders that are meant to be mounted:
    
    def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
    end
    # Add a white list of extensions which are allowed to be uploaded.
    
    def extension_white_list
    %w(jpg jpeg gif png)
    end
    end
    
  48. fog把上傳的存在雲端
    : Configuring the image uploader for production.

    app/uploaders/picture_uploader.rb (Listing 11.64)
     class PictureUploader < CarrierWave::Uploader::Base
    include CarrierWave::MiniMagick
    process resize_to_limit: [400, 400]

    if Rails.env.production?
    storage :fog
    else
    storage :file
    end

    # Override the directory where uploaded files will be stored.

    # This is a sensible default for uploaders that are meant to be mounted:

    def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
    end

    # Add a white list of extensions which are allowed to be uploaded.

    def extension_white_list
    %w(jpg jpeg gif png)
    end
    end


 
about 2 years ago

重點:Action Mailer library
重點:boolean要從false變成true,可以用update_attribute => Listing 10.29: app/controllers/account_activations_controller.rb
重點:參考self的解釋:When to use self in Model?
self
Ruby當中的class method和instance method差在哪?
[rails筆記] ruby 觀念小整理

  1. Account activation:和passwords (Section 8.2) 跟 remember tokens (Section 8.4)的作法很相似。

  2. account activations,不需要用到model (Active Record) (sessions也不用),所以可以直接加在User Model裡面。resources :account_activations, only: [:edit]
    As with sessions (Section 8.1), we’ll model account activations as a resource even though they won’t be associated with an Active Record model. Instead, we’ll include the relevant data (including the activation token and activation status) in the User model. Nevertheless, we’ll interact with account activations via a standard REST URL; because the activation link will be modifying the user’s activation status, we’ll plan to use the edit action.

  3. 預設是false
    add_column :users, :activated, :boolean, default: false

  4. 每個user都需要activation,所以要在user被created之前,就需要先產生activation token and activation digest
    Because every newly signed-up user will require activation, we should assign an activation token and digest to each user object before it’s created.

  5. before_save:每次存檔前(包含create跟update)都會啟動;
    before_create:只有在create之前才會啟動。
    A before_save callback is automatically called before the object is saved, which includes both object creation and updates, but in the case of the activation digest we only want the callback to fire when the user is created. This requires a before_create callback, which we’ll define as follows:

  6. before_create :create_activation_digestmethod reference:在create新用戶前,會先去找這個method。
    This code, called a method reference, arranges for Rails to look for a method called create_activation_digest and run it before creating the user.

  7. create_activation_digest只會被User Model用到,所以要放在private下面。
    Because the create_activation_digest method itself is only used internally by the User model, there’s no need to expose it to outside users; as we saw in Section 7.3.2, the Ruby way to accomplish this is to use the private keyword:

  8. 這邊是借用到8.32的概念 (筆記)因為,activation_token是一個不存在的attribute,所以可以需要用self去新增他,然後new_token這個方法不會用到object,所以他是一個Class method,裡面已經寫好了User.new_token SecureRandom.urlsafe_base64
    This code simply reuses the token and digest methods used for the remember token, as we can see by comparing with the remember method from Listing 8.32:

    self.activation_token  = User.new_token
    self.activation_digest = User.digest(activation_token)
    
    def User.new_token
    SecureRandom.urlsafe_base64
    end
    
  9. 有了user之後,才會有remember token這個功能。
    可是,使用activation_token的時候,還沒有正式新增user。
    所以兩個寫法不同。
    當user註冊的時候,會因為before_create這個功能先去產生activation_token and activation_digest,然後activation_digest會被新增到資料庫裡面去。

    self.activation_token  = User.new_token
    self.activation_digest = User.digest(activation_token)
    
    def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
    end
    

    The main difference is the use of update_attribute in the latter case. The reason for the difference is that remember tokens and digests are created for users that already exist in the database, whereas the before_create callback happens before the user has been created. As a result of the callback, when a new user is defined with User.new (as in user signup, Listing 7.17), it will automatically get both activation_token and activation_digest attributes; because the latter is associated with a column in the database (Figure 10.1), it will be written automatically when the user is saved.

  10. Action Mailer library:$ rails generate mailer UserMailer account_activation password_reset
    (account_activation password_reset 這兩個是method,分別都會有兩個view,一個是純文字檔,一個是HTML檔)
    Mailers跟controller actions很像,會有view。
    With the data modeling complete, we’re now ready to add the code needed to send an account activation email. The method is to add a User mailer using the Action Mailer library, which we’ll use in the Users controller create action to send an email with an activation link. Mailers are structured much like controller actions, with email templates defined as views. Our task in this section is to define the mailers and views with links containing the activation token and email address associated with the account to be activated.
    Here we’ve generated the necessary account_activation method as well as the password_reset method we’ll need in Section 10.2.
    As part of generating the mailer, Rails also generates two view templates for each mailer, one for plain-text email and one for HTML email. For the account activation mailer method, they appear as in Listing 10.6 and Listing 10.7.

  11. 寄出去的啟用信,要包含user的名字(用email找到user的id),以及ativation link。
    As with ordinary views, we can use embedded Ruby to customize the template views, in this case greeting the user by name and including a link to a custom activation link. Our plan is to find the user by email address and then authenticate the activation token, so the link needs to include both the email and the token. Because we’re modeling activations using an Account Activations resource, the token itself can appear as the argument of the named route defined in Listing 10.1:edit_account_activation_url(@user.activation_token, ...)

  12. Listing 10.21:新增用戶時,如果user_param獲得通過,@user若是被save,UserMailer則會執行account_activation這個method,會把@user送到account_activation去。

    app/controllers/users_controller.rb
    class UsersController < ApplicationController
    def create
    @user = User.new(user_params)
    if @user.save
    UserMailer.account_activation(@user).deliver_now
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url
    else
    render 'new'
    end
    end
    end
    
  13. Listing 10.11: (這邊的@user是要送到view去用的)

    app/mailers/user_mailer.rb
    class UserMailer < ApplicationMailer
    def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
    end
    end
    
  14. Listing 10.12: user_mailer.rb送過來的@user,會被用在新用戶的啟動信裡面。

    app/views/user_mailer/account_activation.text.erb
    Hi <%= @user.name %>,
    Welcome to the Sample App! Click on the link below to activate your account:
    <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
    

    這邊,應該是會被送到 Listing 10.29: app/controllers/account_activations_controller.rbedit method

  15. 開始寫edit前的準備工作:
    10.1.3 Activating the account:作法會跟passwords (Listing 8.5)remember tokens (Listing 8.36)類似

    app/controllers/sessions_controller.rb
    user = User.find_by(email: params[:email])
    if user && user.authenticated?(:activation, params[:id])
    

    不過,這邊是寫死的,所以要改成通用的寫法。

    app/models/user.rb(Listing 8.33)
    def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
    end
    

    可以用send去改成這樣:

    app/models/user.rb (Listing 10.24)
    def authenticated?(attribute, token)
    digest = self.send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
    end
    

    因為是在User Model裡面,所以可以把self省略,又可以變成這樣:
    Because we’re inside the user model, we can also omit self, yielding the most idiomatically correct version:

    app/models/user.rb (Listing 10.24)
    def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
    end
    

    (如果沒有token這個變數,可以放''進去,如Listing 10.27)

  16. metaprogramming:用來寫程式的程式
    metaprogramming, which is essentially a program that writes a program.

  17. send method:送出訊息。
    send method, which lets us call a method with a name of our choice by “sending a message” to a given object.

    >> user = User.first
    >> user.activation_digest
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    >> user.send(:activation_digest)
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    >> user.send('activation_digest')
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    >> attribute = :activation
    >> user.send("#{attribute}_digest")
    => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
    
  18. authenticated?寫好後,就可以開始寫edit了:
    With the authenticated? method as in Listing 10.24, we’re now ready to write an edit action that authenticates the user corresponding to the email address in the params hash. Our test for validity will look like this:if user && !user.activated? && user.authenticated?(:activation, params[:id])
    !user.activated?的寫法是要避免已經啟用的account再被啟用ㄧ次。
    Note the presence of !user.activated?, which is the extra boolean alluded to above. This prevents our code from activating users who have already been activated, which is important because we’ll be logging in users upon confirmation, and we don’t want to allow attackers who manage to obtain the activation link to log in as the user.
    If the user is authenticated according to the booleans above, we need to activate the user and update the activated_at timestamp

  19. 新註冊者收到啟用信之後,點選連結,會把activation_tokenemail送回來edit method,如果通過檢驗(authenticated?),就會把:activatedupdatetrue,順便把啟用時間也補上。

    app/views/user_mailer/account_activation.html.erb (Listing 10.13)
    <h1>Sample App</h1>
    <p>Hi <%= @user.name %>,</p>
    <p>Welcome to the Sample App! Click on the link below to activate your account:</p>
    <%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email) %>
    
    app/controllers/account_activations_controller.rb (Listing 10.29)
    class AccountActivationsController < ApplicationController
    def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated,    true)
      user.update_attribute(:activated_at, Time.zone.now)
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
    end
    end
    
    app/models/user.rb (Listing 10.24)
    def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
    end
    
  20. 要讓activation實際有用,就要把他放在登入的地方:有啟用過的用戶才能登入。

    app/controllers/sessions_controller.rb (Listing 10.30)
    class SessionsController < ApplicationController
    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      if user.activated?
        log_in user
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        redirect_back_or user
      else
        message  = "Account not activated. "
        message += "Check your email for the activation link."
        flash[:warning] = message
        redirect_to root_url
      end
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
    end
    end
    
  21. refactor:重構
    Eddie: 可以的話,controller 儘量精簡一些,把實作的邏輯藏到別的地方去(例如 model、service object、form object..etc)
    With the test in Listing 10.31, we’re ready to refactor a little by moving some of the user manipulation out of the controller and into the model. In particular, we’ll make an activate method to update the user’s activation attributes and a send_activation_email to send the activation email. The extra methods appear in Listing 10.33, and the refactored application code appears in Listing 10.34 and Listing 10.35.
    這邊的UserMailer.account_activation(@user).deliver_now可以拿到user model,在model新增一個send_activation_email的method

    app/controllers/users_controller.rb
    class UsersController < ApplicationController
    def create
    @user = User.new(user_params)
    if @user.save
    UserMailer.account_activation(@user).deliver_now
    flash[:info] = "Please check your email to activate your account."
    redirect_to root_url
    else
    render 'new'
    end
    end
    end
    
    app/controllers/users_controller.rb (Listing 10.34)
     class UsersController < ApplicationController
    .
    .
    .
    def create
    @user = User.new(user_params)
    if @user.save
      @user.send_activation_email
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
    end
    .
    .
    .
    end
    

    這邊的user.update_attribute可以拿到user model,在model新增一個activate的method

    app/controllers/account_activations_controller.rb (Listing 10.29)
    class AccountActivationsController < ApplicationController
    def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.update_attribute(:activated,    true)
      user.update_attribute(:activated_at, Time.zone.now)
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
    end
    end
    
    app/controllers/account_activations_controller.rb (Listing 10.35)
    class AccountActivationsController < ApplicationController
    def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Account activated!"
      redirect_to user
    else
      flash[:danger] = "Invalid activation link"
      redirect_to root_url
    end
    end
    end
    
    app/models/user.rb (Listing 10.33)
    class User < ActiveRecord::Base
    def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
    end
    def send_activation_email
    UserMailer.account_activation(self).deliver_now
    end
    end
    

    activate methoduser.update_attribute要把user拿掉,可以改成self,不過self在model內又可以被省略。
    We could have switched from user to self, but recall from Section 6.2.5 that self is optional inside the model.

  22. 我對self的理解:
    等號右邊的self是可以省落的。(self.email = email.downcase)
    當在一個model裡面時,如果不想新增一個新的變數的話,左邊就一定要寫self (self.remember_token)
    Section 10.1.4(在model裡面可以省略self,update_attribute(:activated, true))跟 Section 6.2.5有點矛盾(左邊的self不可省略,email = email.downcase wouldn’t work.),
    Section 6.2.5:self is optional inside the model.
    Section 4.4.2:reverse in the palindrome method
    Section 8.4:self is not optional in an assignment:
    如果要指定自己的attribute,或是不想新增一個local variable的話,就要記得加self。
    不過,只是update_attribute的話,(好像)就不用加self

    class User < ActiveRecord::Base
    attr_accessor :remember_token
    .
    .
    .
    def remember
    self.remember_token = ...
    update_attribute(:remember_digest, ...)
    end
    end
    
    -user.update_attribute(:activated,    true)
    -user.update_attribute(:activated_at, Time.zone.now)
    +update_attribute(:activated,    true)
    +update_attribute(:activated_at, Time.zone.now)
    

    (We could have switched from user to self, but recall from Section 6.2.5 that self is optional inside the model.)

  23. Password reset:
    登入的地方(sessions/new.html.erb)加上"忘記密碼的連結",
    點選後會到一個新的view(views/password_resets/new.html.erb)去輸入email,
    系統(controllers/password_resets_controller.rbcreate method)會根據email找出來這個user,然後會產生digest(存在資料庫)跟token(寄給用戶),之後會寄出一封信,包含一個內含email跟token的連結。當
    用戶點連結之後,會將用戶的email跟token傳送回去edit method作檢查(被分拆掉了,可以看第26點),如果這個用戶是有效的(valid_user)話,就顯示變更密碼的頁面(Figure 10.17),
    戶輸入新密碼(兩次)後,會把新密碼傳回去password_resets_controller.rbupdate method去更新密碼。

  24. 點選忘記密碼的連結,會跑到views/password_resets/new.html.erb的頁面。
    view裡面是這樣寫的:

    app/views/password_resets/new.html.erb
    <% provide(:title, "Forgot password") %>
    <h1>Forgot password</h1>
    <div class="row">
    <div class="col-md-6 col-md-offset-3">
    <%= form_for(:password_reset, url: password_resets_path) do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      <%= f.submit "Submit", class: "btn btn-primary" %>
    <% end %>
    </div>
    </div>
    

    這樣的寫法,會把user的email傳回去資料庫,
    (也可以用params[:password_reset][:email]叫出這個user的email)
    所以new之後,會跑到create去,先用輸入的email找看看有沒有這個user?
    如果有的話,就先為他建一個新的reset_digest,然後寄信給這個user。
    (這邊又要先跑到user那邊先寫create_reset_digestsend_password_reset_email

    app/controllers/password_resets_controller.rb
    class PasswordResetsController < ApplicationController
    def new
    end
    def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
    end
    def edit
    end
    end
    
    app/models/user.rb
     class User < ActiveRecord::Base
    attr_accessor :remember_token, :activation_token, :reset_token
    before_save   :downcase_email
    before_create :create_activation_digest
    # Activates an account.
    
    def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
    end
    # Sends activation email.
    
    def send_activation_email
    UserMailer.account_activation(self).deliver_now
    end
    # Sets the password reset attributes.
    
    def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
    end
    # Sends password reset email.
    
    def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
    end
    private
    # Converts email to all lower-case.
    
    def downcase_email
      self.email = email.downcase
    end
    # Creates and assigns the activation token and digest.
    
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
    end
    
  25. 可以參考10.1.2 Account activation mailer method
    先按忘記密碼(new),輸入email會到create去產生新的token跟寄信,重設密碼的信裡面會有一個到edit method的link:
    edit_password_reset_url(@user.reset_token, email: @user.email)
    http://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=foo%40bar.com
    可以想作:
    edit_user_url
    ,會變成:
    http://example.com/user/1/edit
    這一串亂碼是由new_token產生的,可以當作ID來用。the token will be available in the params hash as params[:id].
    (這邊的reset_token好像沒有加密,忘記由new_token產生的亂碼有沒有加密過了,好像reset_digest = 'digest'(reset_token)這裏的'digest'才會把東西做加密)
    感覺好像是去修改編號(1)user的個人資料。
    感覺好像是去修改編號(亂碼)password_reset的個人資料。
    (這邊記得要有hidden field,才能把email傳回去controller)

  26. 點下去edit_password_reset_url(@user.reset_token, email: @user.email)之後,會回到填寫兩次密碼的地方。兩次密碼輸入之後,會回到update method去做驗證。

  27. activation的時候,只有edit method,可是沒有update method,好像是因為他在edit裡面就直接用user.update_attribute(:activated, true)了(Listing 10.29: App/controllers/account_activations_controller.rb)

  28. password_reset的時候,只有update method,沒有edit method,好像是因為他把edit裡面需要用到的找到user(get_user method)跟認證user (valid_user)都先用before_action處理過了。

    app/controllers/password_resets_controller.rb (Listing 10.51)
    class PasswordResetsController < ApplicationController
    before_action :get_user,   only: [:edit, :update]
    before_action :valid_user, only: [:edit, :update]
    def edit
    end
    private
    def get_user
      @user = User.find_by(email: params[:email])
    end
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end
    end
    
  29. 點下去edit_password_reset_url(@user.reset_token, email: @user.email)之後,會回到填寫兩次密碼的地方。兩次密碼輸入之後,會回到update method去做驗證。
    然後這邊要考慮四種如果:時間過期,成功更新密碼,更新密碼失敗(無效的密碼),更新密碼失敗(確認欄是空白的)
    To define the update action corresponding to the edit action in Listing 10.51, we need to consider four cases: an expired password reset, a successful update, a failed update (due to an invalid password), and a failed update (which initially looks “successful”) due to a blank password and confirmation.

    app/controllers/password_resets_controller.rb
     class PasswordResetsController < ApplicationController
    before_action :get_user,         only: [:edit, :update]
    before_action :valid_user,       only: [:edit, :update]
    before_action :check_expiration, only: [:edit, :update]
    def new
    end
    def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:info] = "Email sent with password reset instructions"
      redirect_to root_url
    else
      flash.now[:danger] = "Email address not found"
      render 'new'
    end
    end
    def edit
    end
    def update
    if params[:user][:password].empty?
      @user.errors.add(:password, "can't be empty")
      render 'edit'
    elsif @user.update_attributes(user_params)
      log_in @user
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'
    end
    end
    private
    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end
    # Before filters
    
    def get_user
      @user = User.find_by(email: params[:email])
    end
    # Confirms a valid user.
    
    def valid_user
      unless (@user && @user.activated? &&
              @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end
    # Checks expiration of reset token.
    
    def check_expiration
      if @user.password_reset_expired?
        flash[:danger] = "Password reset has expired."
        redirect_to new_password_reset_url
      end
    end
    end
    
 
about 2 years ago

重點:9.3:seed the database(9.3.2:faker) + paginate(9.3.3:will_paginate + bootstrap-will_paginate)。
重點:9.4.1:設成boolean,就會自動有?的method。 要記得把boolean的預設值設定為default: false

  1. authentication :allows us to identify users of our site: 成為用戶。需要登入才能看見的頁面,會將網友導到登入頁。9.2.1
  2. authorization :lets us control what they can do:用戶可以做些什麼。非經授權可以瀏覽的頁面,將會被導到首頁。9.2.2
  3. 防止未登入的網友看網頁:before_action(是before filters的一種)在controller所有的actions執行前會先執行。 Before filters use the before_action command to arrange for a particular method to be called before the given actions3 To require users to be logged in, we define alogged_in_user method and invoke it using before_action :logged_in_user, as shown in Listing 9.12.
    app/controllers/users_controller.rb
    class UsersController < ApplicationController
    before_action :logged_in_user, only: [:edit, :update]
    private
    def user_params
    params.require(:user).permit(:name, :email, :password,:password_confirmation)
    end
    # Before filters
    
    # Confirms a logged-in user.
    
    def logged_in_user
    unless logged_in?
    flash[:danger] = "Please log in."
    redirect_to login_url
    end
    end
    end
    
  4. 9.2.1 要登入才能看到某些頁面:logged_in_user
    預設值是只要有before filters,在該controller內的每個action執行之前,都會先執行before filters,除非我們先設定好only這個hash。before_action :logged_in_user, only: [:edit, :update]
    By default, before filters apply to every action in a controller, so here we restrict the filter to act only on the :edit and :update actions by passing the appropriate :only options hash.

  5. 9.2.2 用戶只能看到自己頁面:correct_user
    users should only be allowed to edit their own information.

  6. 設定可以訪問該頁面的正確使用者correct_user

    app/controllers/users_controller.rb
    class UsersController < ApplicationController
    before_action :logged_in_user, only: [:edit, :update]
    before_action :correct_user,   only: [:edit, :update]
    def edit
    end
    def update
    if @user.update_attributes(user_params)
    flash[:success] = "Profile updated"
    redirect_to @user
    else
    render 'edit'
    end
    end
    private
    # Confirms the correct user.
    
    def correct_user
    @user = User.find(params[:id])
    redirect_to(root_url) unless @user == current_user
    end
    end
    
  7. unless @user == current_user可以變成unless current_user?(@user)

    app/helpers/sessions_helper.rb
    def current_user?(user)
    user == current_user
    end
    
  8. 當用戶訪問edit頁面的時候,會先跑before_actioncorrect_user會先抓出來@user,接著跑current_user?(@user)的時候,會把@user送到sessions_helper.rbcurrent_user?(user)去,如果user == current_user是正確的,current_user?(user)會傳true回去correct_user,然後該用戶就可以進去edit page

  9. 9.2.3 Friendly forwarding:登入後,會把用戶轉到用戶本來想要去的頁面。
    要把本來要去的頁面先存在store_location裡面
    In order to forward users to their intended destination, we need to store the location of the requested page somewhere, and then redirect to that location instead of to the default. We accomplish this with a pair of methods, store_location and redirect_back_or, both defined in the Sessions helper (Listing 9.27).

    app/helpers/sessions_helper.rb
    module SessionsHelper
    # Redirects to stored location (or to the default).
    
    def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
    end
    # Stores the URL trying to be accessed.
    
    def store_location
    session[:forwarding_url] = request.url if request.get?
    end
    end
    

    這邊將使用session。按照8.2.1的解釋:Session是Rails內建的方法,session[:user_id] = user.id,這種方式可以把id加密,然後把session[:user_id]存在瀏覽器裡面,以後就可以存取。
    所以session[:forwarding_url] = request.url,也是相同的步驟:將所要求的網址存到session裡面去。
    最後,要把store_location放到logged_in_user method裡面去:如果沒有logged in,會先儲存本來要去的地方在session,然後顯示請登入的訊息,接著轉到登入頁面去。

    app/controllers/users_controller.rb
    def logged_in_user
    unless logged_in?
    store_location
    flash[:danger] = "Please log in."
    redirect_to login_url
    end
    end
    
  10. redirect_back_or(default)method放到sessions_controller.rbcreate去。

    app/controllers/sessions_controller.rb
    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
    end
    
  11. 9.3:seed the database(9.3.2:faker) + paginate(9.3.3:will_paginate + bootstrap-will_paginate)

  12. 用faker gem來新增假用戶 (灌完gem之後,記得要bundle install),清空資料庫 (reset the database:bundle exec rake db:migrate:reset)和重啟資料庫(invoke the Rake task using db:seed =>bundle exec rake db:seed)

  13. create!會在有錯誤的時後跳出例外(exception:主要是為了debug)。
    The create! method is just like the create method, except it raises an exception (Section 6.1.4) for an invalid user rather than returning false. This behavior makes debugging easier by avoiding silent errors.
    參考:save 與 save! / create 與 create!

    其中 save / create,遇到過 validation 時,只會回傳 true 或 false。但有時候我們要 debug 一個表單,有時候一直不知道為何表單為何沒成功一直 false,有時候會使用 save! 或 create! 去 debug。這兩個 method 會 throw ActiveRecord::RecordInvalid exception 中斷程式。明確的告訴你壞在哪邊。

  14. 9.3.3:will_paginate + bootstrap-will_paginate

  15. 在user的view時,will_paginate會自動找尋@user (在不同的view應該是會找不同的@)@users = User.paginate(page: params[:page])
    The will_paginate method is a little magical; inside a users view, it automatically looks for an @users object, and then displays pagination links to access other pages.

  16. 9.4.1:設成boolean,就會自動有?的method。 要記得把boolean的預設值設定為default: false
    We will identify privileged administrative users with a boolean admin attribute in the User model, which will lead automatically to an admin? boolean method to test for admin status. The resulting data model appears in Figure 9.14.

 
over 2 years ago
> [阿芝解法](https://gist.github.com/tienshunlo/3b047979a2ac354e72be)

阿芝解法
最佳解法,單純用html的方法,成功用在select跟checkbox,還沒有成功試在radio上過。
(以後應該在new respond 的地方不用一題一題回答,可以一次送出去很多題,不過就qupid的商業模式好像也沒有必要一次送出去很多題)
radio好像只能用jQuery(JC解法)或無解。
不過,我需要的是,把在後台創建好的東西(issue跟option),放到前台讓user回答(option 跟 respond),所以(好像,目前還沒找到解法)不能用accepts_nested_attributes_for,常常都會發生有重複ID的問題。
在view的地方,還是沒辦法用rails helper (nested, fields_for )傳出去多道題目的答案,結果就是每個答案都有一樣的ID,變成只有一個答案。(可以看一下最下面還沒成功的:view/dashboard/respond/special_cates.html.erb)
因為accepts_nested_attributes_for好像通常都是用在輸入新的data(也就是說,我的後台應該可以用這個方法)。

Rails form with multiple nested models causes issues with radio groups
2009年的這篇,雖然好像可以,可是實際上也不知道有沒有解出來。

accepts_nested_attributes_for好像只能用在has_many,也可以用在has_many::through上
(參考這篇:accepts_nested_attributes_for with Has-Many-Through Relations)

記得select_tag要跟controller搭配好

models/profile_option.rb
class ProfileOption < ActiveRecord::Base
  belongs_to :profile
  belongs_to :option
end
models/profile.rb
class Profile < ActiveRecord::Base
  belongs_to :user
  #可以用的

  has_many :profile_option
  has_many :option, through: :profile_option, dependent: :destroy
  accepts_nested_attributes_for :profile_option
  #可以用的

  self.primary_key = 'user_id'
  GENDER_TYPES = [ "Male", "Female", "Do not wish to say" ]
end

備註:因為profile在設定的時候,沒有自己的id,主要是用user id當作primary_id,所以要加上self.primary_key = 'user_id'這行。

models/option.rb
class Option < ActiveRecord::Base
    belongs_to :issue
    has_many :respond,  :dependent => :destroy
    has_many :user,     through: :respond
    
    #可以用的

    has_many :profile_option
    has_many :profile,  through: :profile_option
    #可以用的

end

備註:@issues = SpecialCate.find(4).issue這個寫法,以後要記得改。

controllers/dashboard/profiles_controller.rb
class  Dashboard::ProfilesController < Dashboard::DashboardController 
  before_action :find_user
  before_action :set_profile
   
  
  
  
  def new
     @profile = @user.build_profile
     #id:4是登入類別

     @issues = SpecialCate.find(4).issue
    
     
  end
  
  
  def create
    @profile = @user.build_profile(profile_params)
    if @profile.save
      #@profile_option = @profile.profile_option.create(:id => current_user ,:profile_id => current_user)

      #redirect_to  dashboard_user_path(current_user)

      redirect_to unresponded_issue_dashboard_user_responds
      #redirect_to special_cates_dashboard_user_responds_path(current_user) 測試直接寫fields_for的方法寫respond,結果寫不出來

    else 
      render new
    end
  end
  
  def edit
   
  end
  
  def update
    
    
    
    ids = params[:profile][:profile_option_attributes].map{|t| t[:option_id]}
    #ids = (params[:ic_old] || {}).keys.map{|i|i.to_i}

    if ids.length > 0
      ProfileOption.where("profile_id =? AND option_id NOT IN (#{ids.join(',')})", @profile.id).delete_all
    else
      ProfileOption.where("profile_id = ?", @profile.id).delete_all
    end
    
    #更新舊的,不過這個APP不需要

    #@profile.profile_option.each do |f|

     # ic = ProfileOption.where(:profile_id => @profile.id, :id => f.id).first

      #if ic

       # ic.update_attributes(:option_id => f.option_id)

      #end

    #end

    
    #加入新的作法一

    params[:profile][:profile_option_attributes].each do |index|
      if !@profile.profile_option.map(&:option_id).include?(index[:option_id].to_i)
        ProfileOption.create(:profile_id => @profile.id, :option_id => index[:option_id])
      end
    end
    
    #加入新的作法二

    #params[:profile][:profile_option_attributes].each_with_index do |tar, index|

     # if !@profile.profile_option.map(&:option_id).include?(tar[:option_id].to_i)

      #  ProfileOption.create(:profile_id => @profile.id, :option_id => tar[:option_id])

      # end

    #end

    
    redirect_to  dashboard_user_path(current_user)
    
    
    #scattfold的普通版本

    #if @profile.update(profile_params)

        #redirect_to  dashboard_user_path(current_user)

    #else

        #render edit

    #end

        
  end
  
  private
  def profile_params
    
   
    #可以用的

    params.require(:profile).permit(:location, :gender, profile_option_attributes:[:id, :option_id])
    #可以用的

    
    #params.require(:profile).permit(:location, :gender, :option_ids => [])

    #params.require(:profile).permit(:location, :gender, :option_ids => [], :basic_option_a, :basic_option_b, :basic_option_c, :basic_option_d, :basic_option_e)


  end
  
  def find_user
    @user = current_user
  end
  
  def set_profile
    @profile = @user.profile
  end
  
end

views/dashboard/profiles/new.html.erb
<div class='content_wrapper option_wrapper'> 
    <ul class = 'option_first_banner clearfix'>
        <li class = 'option_right_banner'>
            <%= form_for @profile, :url => dashboard_user_profiles_path, :method => :POST do |f| %>
                <div class="edit_panel panel panel-default">
                    <div class="edit_heading panel-heading">
                        <h3 class="panel-title">個人資料</h3>
                    </div>
                    <div class="panel-body">
                        <div class='option_content'>
                            <div class="form-group">
                                <%= label_tag "地點:" %> <%= f.text_field :location, class:"form-control" %>
                            </div>
                            <div class="form-group">
                                <%= label_tag "性別:" %> <%= f.select :gender, Profile::GENDER_TYPES, class:"form-control" %>
                            </div>
                            <% @issues.each do |issue|%>
                            <div class="form-group">
                                <%= issue.content %>  
                                <%= select_tag "profile[profile_option_attributes][][option_id]", options_from_collection_for_select(issue.option, "id" ,"content"), {:class =>"form-control"} %>
                            </div>
                            <% end %>
                        </div>
                        <div class="form-group">
                            <%= f.submit "送出", class: "btn btn-primary" %>
                        </div>
                    </div>
                </div>
            <% end %>
        </li>
    </ul>
</div>
view/dashboard/respond/special_cates.html.erb
<div class='content_wrapper option_wrapper'> 
<ul class = 'option_first_banner clearfix'>
    <li class = 'option_right_banner'>
        <%= form_for @user, :url => dashboard_user_responds_path, :method => :POST do |f| %>
        
            <div class="edit_panel panel panel-default">
                <div class="edit_heading panel-heading">
                  
                    <h3 class="panel-title">
                      
                    </h3>
                </div>
                <div class="panel-body">
                  <div class='option_content'>
                    <%= f.fields_for @respond do |ff|%>
                    <% @issues.each do |issue|%>
                      <%= label_tag issue.id %>
                      <%= label_tag issue.content %>
                      <%# issue.option.each do |option| %>
                        <div class="form-group">
                          <div class="radio">
                            <%#= f.radio_button :option_id, option.id %><%#= label_tag option.content %>
                            <%#= ff.radio_button :option_id, option.id %><%#= label_tag option.content %>
                            <%= ff.select :option_id, options_from_collection_for_select(issue.option, "id" ,"content"), {:class =>"form-control"} %>
                          </div>   
                        </div>
                      <% end %>    
                    <%# end %>
                    <% end %>
                  </div>
                    <div class="form-group">
                        <%= f.submit "送出", class: "btn btn-primary" %>
                    </div>
                </div>
            </div>
        <% end %>
    </li>
</ul>
</div>


params:

 
over 2 years ago

用戶前台:

  1. 登入: - 帳號密碼:devise
    - 個人資訊:has_one :profile,地點、性別、生日 - 準備題:同時送出五題單選題 (暫定使用select_tag) (這邊要參考阿之的解法,記得select_tag要跟controller搭配好,實際作法是這樣:profile/new.html.erb
  2. 回答配對問題:點選想要回答的問題,進去個別頁面後,一次回答一個的單選題 (暫定使用check_box_tag) - 有全部未答過問題的index page。(要把SpecialCates的問題排除掉。在個別頁面的地方要檢查一下,如果以前回答過,就應該跳回去index)
    - 可能要分作價值觀、戀愛觀、金錢觀等分頁:尚未決定 - 可能要分作今日新題目、時事題、特殊題:尚未決定,但應該都是一頁回答一個問題。
  3. 用戶後台: - 可以看到自己已經回答過的問題,然後可以一次edit一個問題的答案。
    - 可以看到自己的profile,然後決定要不要edit自己的準備題。(profile edit的code可以用profile new的code)
  4. 其他用戶前台(User show page):可以看到其他用戶回答過的問題與答案。 - 如果自己有回答過的話,可以看到自己的答案,也可以選擇要不要換答案。 - 如果自己沒有回答過的話,可以選擇要不要回答。
    - 可以看到其他用戶的profile跟準備題 (不過這邊不能重複使用profile new的code) (這邊還沒弄好)

http://guides.rubyonrails.org/form_helpers.html
3.3 Option Tags from a Collection of Arbitrary Objects

<% cities_array = City.all.map { |city| [city.name, city.id] } %>
<%= options_for_select(cities_array) %>

http://blog.xdite.net/posts/2012/02/06/how-to-design-select-in-rails
http://blog.jex.tw/blog/2015/08/02/rails-view/

 <div class="form-group">
                    <%= label_tag "類別:" %> <%= f.select :cate_ids, Issue::CATES_ARRAY, { }, {:class =>"form-control", :multiple => true} %>
              </div>
              <div class="form-group">
                    <%= label_tag "一般類別:" %> <%= f.select :ordinary_cate_ids, Issue::ORDINARY_CATES_ARRAY, { }, {:class =>"form-control", :multiple => true} %>
              </div>
              <div class="form-group">
                    <%= label_tag "特殊類別:" %> <%= f.select :special_cate_ids, Issue::SPECIAL_CATES_ARRAY, { include_blank: true }, {:class =>"form-control", :multiple => true} %>
              </div>
Started GET "/dashboard/admin/issues/new" for 218.161.40.151 at 2016-06-15 08:25:02 +0000
Cannot render console from 218.161.40.151! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Dashboard::Admin::IssuesController#new as HTML
  Manager Load (0.7ms)  SELECT  "managers".* FROM "managers" WHERE "managers"."id" = ?  ORDER BY "managers"."id" ASC LIMIT 1  [["id", 1]]
  Rendered dashboard/admin/issues/new.html.erb within layouts/admin (33.3ms)
  Rendered common/_managernavbar.html.erb (0.7ms)
  Rendered common/_footer.html.erb (0.2ms)
Completed 200 OK in 1199ms (Views: 1188.3ms | ActiveRecord: 0.7ms)
Started GET "/dashboard/admin/issues/new" for 218.161.40.151 at 2016-06-15 08:14:27 +0000
Cannot render console from 218.161.40.151! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Dashboard::Admin::IssuesController#new as HTML
  Manager Load (0.8ms)  SELECT  "managers".* FROM "managers" WHERE "managers"."id" = ?  ORDER BY "managers"."id" ASC LIMIT 1  [["id", 1]]
  Rendered dashboard/admin/issues/new.html.erb within layouts/admin (34.6ms)
  Rendered common/_managernavbar.html.erb (1.0ms)
  Rendered common/_footer.html.erb (0.6ms)
Completed 200 OK in 1118ms (Views: 1075.2ms | ActiveRecord: 0.8ms)
Started GET "/dashboard/admin/issues/new" for 218.161.40.151 at 2016-06-15 08:22:42 +0000
Cannot render console from 218.161.40.151! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Dashboard::Admin::IssuesController#new as HTML
  Manager Load (0.4ms)  SELECT  "managers".* FROM "managers" WHERE "managers"."id" = ?  ORDER BY "managers"."id" ASC LIMIT 1  [["id", 1]]
  Cate Load (0.5ms)  SELECT "cates".* FROM "cates"
  OrdinaryCate Load (0.3ms)  SELECT "ordinary_cates".* FROM "ordinary_cates"
  SpecialCate Load (0.3ms)  SELECT "special_cates".* FROM "special_cates"
  Rendered dashboard/admin/issues/new.html.erb within layouts/admin (40.4ms)
  Rendered common/_managernavbar.html.erb (1.0ms)
  Rendered common/_footer.html.erb (0.3ms)
Completed 200 OK in 1405ms (Views: 1350.6ms | ActiveRecord: 1.9ms)
Started GET "/dashboard/admin/issues/new" for 218.161.40.151 at 2016-06-15 08:23:46 +0000
Cannot render console from 218.161.40.151! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Dashboard::Admin::IssuesController#new as HTML
  Manager Load (20.5ms)  SELECT  "managers".* FROM "managers" WHERE "managers"."id" = ?  ORDER BY "managers"."id" ASC LIMIT 1  [["id", 1]]
  Cate Load (9.0ms)  SELECT "cates".* FROM "cates"
  OrdinaryCate Load (0.5ms)  SELECT "ordinary_cates".* FROM "ordinary_cates"
  SpecialCate Load (1.5ms)  SELECT "special_cates".* FROM "special_cates"
  Rendered dashboard/admin/issues/new.html.erb within layouts/admin (84.1ms)
  Rendered common/_managernavbar.html.erb (0.9ms)
  Rendered common/_footer.html.erb (0.3ms)
Completed 200 OK in 2669ms (Views: 2598.7ms | ActiveRecord: 31.4ms)
 
over 2 years ago

來紀錄一下我的後台是怎麼新增Cate(類別), Issue(題目), Option(選項)。

剛開始的時候,因為以為題目(Issue)只會屬於一個類別(Cate),所以寫成has_many/belongs_to的關係。

不過,後來想到,如果有些題目要設成:重要題/基本題/特殊題/featured,又或者,要在不同的頁面出現,譬如:登入頁面 / 一般頁面等等可能,可能一個題目就要有不同的類別,所以決定要改成has_many, through:的做法。
(has_many, through:比HABTM好的理由看這裡)

但是,要有多重類別,又有不同的做法。
最簡單的做法:應該是讓一個issue有很多個attribute,譬如cate_1、cate_2、cate_3、cate_4、cate_5、cate_6,然後每一個都放進去一個的cate。
不過好像有點蠢。因為以後如果有新增的,就必須要一直rails g migration add_cate_n_to_issue_table。

所以比較好的做法應該有兩種(利用has_many, through:),但是我還沒想出來哪個比較好:

  • 第一種做法:

  • 第二種做法:等於是最簡單的做法加上第一種做法

    • 一個題目有兩個(或以上的)category columns(attributes)
    • 一個可以叫ordinary category,一個可以叫special category
    • 也是可以選擇要不要用{:multiple => true}

備註:目前為止,還沒有辦法找到用rails helper(nested: fields_for)(好像不能用在has_many::through這樣的關係上)同時傳送出多個題目的答案的方法,因為id都會重複,必須要用html tag的方式去解決,詳細情況請看這篇:用select tag 一次回答多個問題

  1. 舊的:題目只會屬於一個類別,所以寫成has_many/belongs_to。

    surveysays / app / models / cate.rb
    class Cate < ActiveRecord::Base
    has_many :issue#, :dependent => :destroy
    
    end
    
    surveysays / app / models / issue.rb
    class Issue < ActiveRecord::Base
    belongs_to :cate
    has_many :option, :dependent => :destroy
    has_many :respond, through: :option
    has_many :user, through: :respond
    
    accepts_nested_attributes_for :option, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
    end
    

    重要:select的寫法:(這邊可能寫錯了,怎麼會沒有f.)
    <%= select('issue', 'cate_id', Cate.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }, {:class =>"form-control"})%>

    surveysays / app / views / dashboard / admin / issues / new.html.erb
    <%= form_for @issue, :url => dashboard_admin_issues_path, :method => :POST do |f| %>
                <div class="edit_panel panel panel-default">
                    <div class="edit_heading panel-heading">
                        <h3 class="panel-title">建新問題</h3>
          </div>
          <div class="panel-body">
            <div class='option-content'>
                <div class="form-group">
                    <%= label_tag "類別:" %> <%= select('issue', 'cate_id', Cate.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }, {:class =>"form-control"})%>
              </div>
              <div class="form-group">
                                <%= label_tag "題目:" %> <%= f.text_field :content, class:"form-control" %>
                            </div>
                            <div class="form-inline">
                                <div class="form-group">
                                    <%= f.fields_for :option do |option| %>
                                        <p>
                                        <%= label_tag "選項:" %>
                                        <%= option.text_field :content, class:"form-control" %>
                                        <%= option.check_box :_destroy, class:"checkbox" %><%= label_tag "刪除" %>
                                        <%#= option.link_to_remove "Remove this Answer" %>
                                        </p>
                                    <% end %>
                                </div>
                            </div>   
            </div>
            <div class="form-group">
                <%= f.submit "送出", class: "btn btn-primary" %>
            </div>
           </div>
          </div>
        <% end %>
    

    注意:這邊是:cate_id,因為只有一個cate_id會傳出去。

    surveysays / app / controllers / dashboard / admin / issues_controller.rb
    private
    def issue_params
        params.require(:issue).permit(:content, :cate_id, option_attributes:[:issue_id, :id, :content,:_destroy])
    end
    
  2. 第一種做法:要新增一個cate_issue.rb,然後

    surveysays / app / models / cate.rb
    class Cate < ActiveRecord::Base
    has_many :cate_issue
    has_many :issue, through: :cate_issue
    end
    
    surveysays / app / models / issue.rb (把belongs_to :cate注記掉)
    class Issue < ActiveRecord::Base
    has_many :cate_issue
    has_many :cate, through: :cate_issue
    
    #belongs_to :cate
    
    has_many :option, :dependent => :destroy
    has_many :respond, through: :option
    has_many :user, through: :respond
    
    accepts_nested_attributes_for :option, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
    end
    
    surveysays / app / models / cate_issue.rb
    class CateIssue < ActiveRecord::Base
    belongs_to :cate
    belongs_to :issue
    end
    

    注意:這邊記得要改成:cate_ids => [],因為會傳出去很多個cate ids

    surveysays / app / controllers / dashboard / admin / issues_controller.rb
    private
    def issue_params
        params.require(:issue).permit(:content, :cate_ids => [], option_attributes:[:issue_id, :id, :content,:_destroy])
    end
    

    <%= label_tag "類別:" %> <%= f.select :cate_ids, Cate.all.collect {|p| [ p.name, p.id ] }, { }, {:class =>"form-control", :multiple => true} %>

    surveysays / app / views / dashboard / admin / issues / new.html.erb
    <%= form_for @issue, :url => dashboard_admin_issues_path, :method => :POST do |f| %>
                <div class="edit_panel panel panel-default">
                    <div class="edit_heading panel-heading">
                        <h3 class="panel-title">
                            建新問題
                        </h3>
          </div>
          <div class="panel-body">
            <div class='option-content'>
              <div class="form-group">
                    <%= label_tag "類別:" %> 
                <%= f.select :cate_ids, Cate.all.collect {|p| [ p.name, p.id ] }, { }, {:class =>"form-control", :multiple => true} %>
              </div>
              <div class="form-group">
                                <%= label_tag "題目:" %> 
                <%= f.text_field :content, class:"form-control" %>
                            </div>
                            <div class="form-inline">
                                <div class="form-group">
                                    <%= f.fields_for :option do |option| %>
                                        <p>
                                        <%= label_tag "選項:" %>
                                        <%= option.text_field :content, class:"form-control" %>
                                        <%= option.check_box :_destroy, class:"checkbox" %>
                    <%= label_tag "刪除" %>
                                        <%#= option.link_to_remove "Remove this Answer" %>
                                        </p>
                                    <% end %>
                                </div>
                            </div>   
            </div>
            <div class="form-group">
                <%= f.submit "送出", class: "btn btn-primary" %>
            </div>
           </div>
          </div>
        <% end %>
    
  3. 第二種做法:要新增一個cate_issue.rb(應該要改個名字比較好,不過還沒想到)跟另一個category的名字。
    記得:改成OrdinaryCategory.rb,SpecialCategory.rb,Issue.rb,CateIssue.rb (先保留Cate.rb)
    記得:改routes,要在cateissue table 新增ordinary_cate_id 跟special_cate_id

    surveysays / app / models / cate.rb
    class Cate < ActiveRecord::Base
    has_many :cate_issue
    has_many :issue, through: :cate_issue
    end
    
    surveysays / app / models / ordinary_category.rb
    class OrdinaryCate < ActiveRecord::Base
    has_many :cate_issue
    has_many :issue, through: :cate_issue
    end
    
    surveysays / app / models / special_category.rb
    class SpecialCate < ActiveRecord::Base
    has_many :cate_issue
    has_many :issue, through: :cate_issue
    end
    
    surveysays / app / models / issue.rb
    class Issue < ActiveRecord::Base
    has_many :cate_issue
    has_many :cate, through: :cate_issue
    has_many :ordinary_cate, through: :cate_issue
    has_many :special_cate, through: :cate_issue
    
    #belongs_to :cate
    
    has_many :option, :dependent => :destroy
    has_many :respond, through: :option
    has_many :user, through: :respond
    
    accepts_nested_attributes_for :option, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
    end
    
    surveysays / app / models / cate_issue.rb
    class CateIssue < ActiveRecord::Base
    belongs_to :ordinary_cate
    belongs_to :specail_cate
    belongs_to :cate
    belongs_to :issue
    end
    

    注意:這邊記得要改成:cate_ids => [],因為會傳出去很多個cate ids

    surveysays / app / controllers / dashboard / admin / issues_controller.rb
    private
    def issue_params
        params.require(:issue).permit(:content, :cate_ids => [],:ordinary_cate_ids => [],:special_cate_ids => [], option_attributes:[:issue_id, :id, :content,:_destroy])
    end
    
    surveysays / app / views / dashboard / admin / issues / new.html.erb
    <%= form_for @issue, :url => dashboard_admin_issues_path, :method => :POST do |f| %>
                <div class="edit_panel panel panel-default">
                    <div class="edit_heading panel-heading">
                        <h3 class="panel-title">
                            建新問題
                        </h3>
          </div>
          <div class="panel-body">
            <div class='option-content'>
              <div class="form-group">
                    <%= label_tag "類別:" %> <%= f.select :cate_ids, Cate.all.collect {|p| [ p.name, p.id ] }, { }, {:class =>"form-control", :multiple => true} %>
              </div>
              <div class="form-group">
                    <%= label_tag "一般類別:" %> <%= f.select :ordinary_cate_ids, OrdinaryCate.all.collect {|p| [ p.name, p.id ] }, { }, {:class =>"form-control", :multiple => true} %>
              </div>
              <div class="form-group">
                    <%= label_tag "特殊類別:" %> <%= f.select :special_cate_ids, SpecialCate.all.collect {|p| [ p.name, p.id ] }, { }, {:class =>"form-control", :multiple => true} %>
              </div>
              <div class="form-group">
                                <%= label_tag "題目:" %> <%= f.text_field :content, class:"form-control" %>
                            </div>
                            <div class="form-inline">
                                <div class="form-group">
                                    <%= f.fields_for :option do |option| %>
                                        <p>
                                        <%= label_tag "選項:" %>
                                        <%= option.text_field :content, class:"form-control" %>
                                        <%= option.check_box :_destroy, class:"checkbox" %><%= label_tag "刪除" %>
                                        <%#= option.link_to_remove "Remove this Answer" %>
                                        </p>
                                    <% end %>
                                </div>
                            </div>   
            </div>
            <div class="form-group">
                <%= f.submit "送出", class: "btn btn-primary" %>
            </div>
           </div>
          </div>
        <% end %>
    

    注意:這邊寫法要注意

    surveysays / app / controllers / dashboard / admin / ordinary_cates_controller.rb
    private
     def ordinarycate_params
         params.require(:ordinary_cate).permit(:name)
     end
    
    surveysays / app / controllers / dashboard / admin / special_cates_controller.rb
    private
     def specialcate_params
         params.require(:special_cate).permit(:name)
     end
    
 
over 2 years ago

最重要參考:Rails 3: Multiple Select with has_many through associations

參考1.:document 在這

參考2.:HAS_MANY :THROUGH TUTORIAL

參考3.:Building a has_many, through model relationship and form with Cocoon

參考4.:Deep Nested Attributes Using Has_many Through Join Model

參考5.:accepts_nested_attributes_for with Has-Many-Through Relations
參考5重點節錄:The inverse_of option allows you to tell Rails when two model relations describe the same relationship, but from opposite directions. For example, if a User has_many :posts and a Post belongs_to :user, you can tell Rails that the :user relation on Post is the inverse of the :posts relation on User.

參考6.:Complex Rails Forms with Nested Attributes
參考6重點節錄:Please note the changed variable name for the fields_for block—ff rather than f. In this case for a has_one relationship, the logic is “if an address exists, show a field to edit the city attribute. Otherwise if there is no address, don’t show any fields.” Here we hit our first stumbling block: if the fields are hidden when there is no address, how do we create an address record in the first place? Since this is a view problem (do we display fields or not?), we want to solve this problem in the view layer. We do this by setting up default values for the form objectin a helper:

app/helpers/form_helper.rb
module FormHelper
def setup_user(user)
user.address ||= Address.new
user
end
end
app/views/users/_form.html.erb
<%= form_for(setup_user(user)) do |f| %>