A proto-feed

雖然 micropost 表單現在可以運作了,但是使用者無法立即看到成功建立的 micropost,因為在 Home page 並不會顯示任何 microposts。雖然其實可以透過「view my profile」看到所有 microposts,不過還是不太直覺。

如果能在 Home page 直接顯示使用者的所有 microposts 列表(a feed of microposts)就好了,mockup 如下:

Chapter 12 我們會建立一個包含被使用者關注的其他使用者的 microposts feed。

因為每個使用者都會有 feed,所以可以在 User model 定義一個 feed 方法,尋找目前使用者發表的所有 microposts。我們會透過在 Micropost model 調用 where 方法來實現:

app/models/user.rb

class User < ActiveRecord::Base
  .
  .
  .
  # Defines a proto-feed.
  # See "Following users" for the full implementation.
  def feed
    Micropost.where("user_id = ?", id)
  end

    private
    .
    .
    .
end

其中的「?」:

Micropost.where("user_id = ?", id)

是要確保 id 的值在傳入底層的 SQL query 之前做了適當的轉義(escaped),可以避免 SQL injection 這種嚴重的安全漏洞。這裡用到的 id 屬性只是一個整數(例如,self.id 是使用者的唯一 ID),所以沒什麼危險。不過在 SQL 述句中引入變數之前做轉義是個好習慣。

細心的讀者這時候可能會注意上述程式碼的寫法實際上可以寫成:

def feed
  microposts
end

我們會沿用之前的寫法,因為它涵蓋更完善的在 Chapter 12 所需的完整 feed。

為了在 sample apllication 使用 feed,我們要增加一個 @feed_items 實例變數:

@feed_items = current_user.feed.paginate(page: params[:page])

然後在 Home page 加上 feed partial。

注意,現在必須執行兩行程式碼,來判斷使用者是否登入,也就是說原本在 StaticPages controller 的 home action:

@micropost = current_user.microposts.build if logged_in?

改成:

if logged_in?
    @micropost  = current_user.microposts.build
    @feed_items = current_user.feed.paginate(page: params[:page])
  end

當使用者登入時,這兩行程式碼才會被執行。

因此我們必須在 StaticPages controller 的 home action 加上 if-end 述句:

app/controllers/static_pages_controller.rb

class StaticPagesController < ApplicationController

  def home
    if logged_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end

  def help
  end

  def about
  end

  def contact
  end
end

接著建立 feed partial:

app/views/shared/_feed.html.erb

<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

feed partial 使用以下的程式碼,把單篇 micropost 交給 app/views/microposts/_micropost.html.erb 的 partial 渲染:

<%= render @feed_items %>

Rails 會知道要去渲染 micropost partail,是因為 @feed_items 中的元素都是 Micropost class 的實例。所以 Rails 會在對應 resource 的 views 資料夾中尋找正確的 partial:

app/views/microposts/_micropost.html.erb

最後就把 feed partial 加進 Home page 裡:

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 class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>
  </div>
<% else %>
  .
  .
  .
<% end %>

最後 Home page 畫面如下:

現在,發布 micropost 可以按照預想的方式使用了,如下圖:

不過還有個不足,如果發布失敗,Home page 還需要一個 @feed_items 實例變數,所以提交失敗時網站無法正常執行:

最簡單的解決方式是,如果提交失敗就把 @feed_items 設為空陣列:

app/controllers/microposts_controller.rb

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

但是這麼做,分頁連結就失效了,可以點擊分頁連結,看是什麼原因。