# 11.2 顯示微博
盡管我們還沒實現直接在網頁中發布微博的功能(將在 [11.3.2 節](#creating-microposts)實現),不過還是有辦法顯示微博,并對顯示的內容進行測試。我們將按照 Twitter 的方式,不在微博資源的 `index` 頁面顯示用戶的微博,而在用戶資源的 `show` 頁面顯示,構思圖如[圖 11.4](#fig-user-microposts-mockup) 所示。我們會先使用一些簡單的 ERb 代碼,在用戶的資料頁面顯示微博,然后在 [9.3.2 節](chapter9.html#sample-users)的種子數據中添加一些微博,這樣才有內容可以顯示。
圖 11.4:顯示有微博的資料頁面構思圖
## 11.2.1 渲染微博
我們計劃在用戶的資料頁面(`show.html.erb`)顯示用戶的微博,還要顯示用戶發布了多少篇微博。你會發現,很多做法和 [9.3 節](chapter9.html#showing-all-users)列出所有用戶時類似。
雖然 [11.3 節](#manipulating-microposts)才會用到微博控制器,但馬上就需要使用視圖,所以現在就要生成控制器:
```
$ rails generate controller Microposts
```
這一節的主要目的是渲染用戶發布的所有微博。[9.3.5 節](chapter9.html#partial-refactoring)用過這樣的代碼:
```
<ul class="users">
<%= render @users %>
</ul>
```
這段代碼會自動使用局部視圖 `_user.html.erb` 渲染 `@users` 變量中的每個用戶。同樣地,我們要編寫 `_micropost.html.erb` 局部視圖,使用類似的方式渲染微博集合:
```
<ol class="microposts">
<%= render @microposts %>
</ol>
```
注意,我們使用的是有序列表標簽 `ol`(而不是無需列表 `ul`),因為微博是按照一定順序顯示的(按時間倒序)。相應的局部視圖如[代碼清單 11.21](#listing-micropost-partial) 所示。
##### 代碼清單 11.21:渲染單篇微博的局部視圖
app/views/microposts/_micropost.html.erb
```
<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>
```
這個局部視圖使用了 `time_ago_in_words` 輔助方法,這個方法的作用應該很明顯,效果會在 [11.2.2 節](#sample-microposts)看到。[代碼清單 11.21](#listing-micropost-partial) 還為每篇微博指定了 CSS ID:
```
<li id="micropost-<%= micropost.id %>">
```
這是好習慣,說不定以后要處理(例如使用 JavaScript)單篇微博呢。
接下來要解決顯示大量微博的問題。我們可以使用 [9.3.3 節](chapter9.html#pagination)顯示大量用戶的方法來解決這個問題,即使用分頁。和前面一樣,我們要使用 `will_paginate` 方法:
```
<%= will_paginate @microposts %>
```
如果和用戶列表頁面的代碼([代碼清單 9.41](chapter9.html#listing-will-paginate-index-view))比較的話,會發現之前使用的代碼是:
```
<%= will_paginate %>
```
前面之所以可以直接調用,是因為在用戶控制器中,`will_paginate` 假定有一個名為 `@users` 的實例變量([9.3.3 節](chapter9.html#pagination)說過,這個變量所屬的類應該是 `AvtiveRecord::Relation`)。現在,因為還在用戶控制器中,但是我們要分頁顯示微博,所以必須明確地把 `@microposts` 變量傳給 `will_paginate` 方法。當然了,我們還要在 `show` 動作中定義 `@microposts` 變量,如[代碼清單 11.22](#listing-user-show-microposts-instance) 所示。
##### 代碼清單 11.22:在用戶控制器的 `show` 動作中定義 `@microposts` 變量
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
.
.
.
def show
@user = User.find(params[:id])
@microposts = @user.microposts.paginate(page: params[:page]) end
.
.
.
end
```
注意看 `paginate` 方法是多么智能,甚至可以在關聯上使用,從 `microposts` 表中取出每一頁要顯示的微博。
最后,還要顯示用戶發布的微博數量。我們可以使用 `count` 方法實現:
```
user.microposts.count
```
和 `paginate` 方法一樣,`count` 方法也可以在關聯上使用。`count` 的計數過程不是把所有微博都從數據庫中讀取出來,然后再在所得的數組上調用 `length` 方法,如果這樣做的話,微博數量一旦很多,效率就會降低。其實,`count` 方法直接在數據庫層計算,讓數據庫統計指定的 `user_id` 擁有多少微博。(所有數據庫都會對這種操作做性能優化。如果統計數量仍然是應用的性能瓶頸,可以使用“[計數緩存](http://railscasts.com/episodes/23-counter-cache-column)”進一步提速。)
綜上所述,現在可以把微博添加到資料頁面了,如[代碼清單 11.23](#listing-user-show-microposts) 所示。注意,`if @user.microposts.any?`(在[代碼清單 7.19](chapter7.html#listing-errors-partial) 中見過類似的用法)的作用是,如果用戶沒有發布微博,不顯示一個空列表。
##### 代碼清單 11.23:在用戶資料頁面中加入微博
app/views/users/show.html.erb
```
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
</aside>
<div class="col-md-8">
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div> </div>
```
現在,我們可以查看一下修改后的用戶資料頁面,如[圖 11.5](#fig-user-profile-no-microposts)。可能會出乎你的意料,不過也是理所當然的,因為現在還沒有微博。下面我們就來改變這種狀況。
圖 11.5:添加顯示微博的代碼后用戶的資料頁面,但沒有微博
## 11.2.2 示例微博
在 [11.2.1 節](#rendering-microposts),為了顯示用戶的微博,創建或修改了幾個模板,但是結果有點不給力。為了改變這種狀況,我們要在 [9.3.2 節](chapter9.html#sample-users)用到的種子數據中加入一些微博。
為所有用戶添加示例微博要花很長時間,所以我們決定只為前六個用戶添加。為此,要使用 `take` 方法:
```
User.order(:created_at).take(6)
```
調用 `order` 方法的作用是按照創建用戶的順序查找六個用戶。
我們要分別為這六個用戶創建 50 篇微博(數量要多于 30 個才能分頁)。為了生成微博的內容,我們要使用 Faker 提供的 [`Lorem.sentence`](http://rubydoc.info/gems/faker/1.3.0/Faker/Lorem) 方法。[[2](#fn-2)]添加示例微博后的種子數據如[代碼清單 11.24](#listing-sample-microposts) 所示。
##### 代碼清單 11.24:添加示例微博
db/seeds.rb
```
.
.
.
users = User.order(:created_at).take(6)
50.times do
content = Faker::Lorem.sentence(5)
users.each { |user| user.microposts.create!(content: content) }
end
```
然后,像之前一樣重新把種子數據寫入開發數據庫:
```
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
```
完成后還要重啟 Rails 開發服務器。
現在,我們能看到 [11.2.1 節](#rendering-microposts)的勞動成果了——用戶資料頁面顯示了微博。[[3](#fn-3)]初步結果如[圖 11.6](#fig-user-profile-microposts-no-styling) 所示。
圖 11.6:用戶資料頁面顯示的微博,還沒添加樣式
[圖 11.6](#fig-user-profile-microposts-no-styling) 中顯示的微博還沒有樣式,那我們就加入一些樣式,如[代碼清單 11.25](#listing-micropost-css) 所示,[[4](#fn-4)]然后再看一下頁面顯示的效果。
##### 代碼清單 11.25:微博的樣式(包含本章要使用的所有 CSS)
app/assets/stylesheets/custom.css.scss
```
.
.
.
/* microposts */
.microposts {
list-style: none;
padding: 0;
li {
padding: 10px 0;
border-top: 1px solid #e8e8e8;
}
.user {
margin-top: 5em;
padding-top: 0;
}
.content {
display: block;
margin-left: 60px;
img {
display: block;
padding: 5px 0;
}
}
.timestamp {
color: $gray-light;
display: block;
margin-left: 60px;
}
.gravatar {
float: left;
margin-right: 10px;
margin-top: 5px;
}
}
aside {
textarea {
height: 100px;
margin-bottom: 5px;
}
}
span.picture {
margin-top: 10px;
input {
border: 0;
}
}
```
[圖 11.7](#fig-user-profile-with-microposts) 是第一個用戶的資料頁面,[圖 11.8](#fig-other-profile-with-microposts) 是另一個用戶的資料頁面,[圖 11.9](#fig-user-profile-microposts) 是第一個用戶資料頁面的第 2 頁,頁面底部還顯示了分頁鏈接。注意觀察這三幅圖,可以看到,微博后面顯示了距離發布的時間(例如,“Posted 1 minute ago.”),這就是[代碼清單 11.21](#listing-micropost-partial) 中 `time_ago_in_words` 方法實現的效果。過一會再刷新頁面,這些文字會根據當前時間自動更新。
圖 11.7:顯示有微博的用戶資料頁面([/users/1](http://localhost:3000/users/1))圖 11.8:另一個用戶的資料頁面([/users/5](http://localhost:3000/users/5)),也顯示有微博圖 11.9:微博分頁鏈接([/users/1?page=2](http://localhost:3000/users/1?page=2))
## 11.2.3 資料頁面中微博的測試
新激活的用戶會重定向到資料頁面,那時已經測試了資料頁面是否能正確渲染([代碼清單 10.31](chapter10.html#listing-signup-with-account-activation-test))。本節,我們要編寫幾個簡短的集成測試,檢查資料頁面中的其他內容。首先,生成資料頁面的集成測試文件:
```
$ rails generate integration_test users_profile
invoke test_unit
create test/integration/users_profile_test.rb
```
為了測試資料頁面中顯示有微博,我們要把微博固件和用戶關聯起來。Rails 提供了一種便利的方法,可以在固件中建立關聯,例如:
```
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
```
把 `user` 的值設為 `michael` 后,Rails 會把這篇微博和指定的用戶固件關聯起來:
```
michael:
name: Michael Example
email: michael@example.com
.
.
.
```
為了測試微博分頁,我們要使用[代碼清單 9.43](chapter9.html#listing-users-fixtures-extra-users) 中用到的方法,通過嵌入式 Ruby 代碼多生成一些微博固件:
```
<% 30.times do |n| %>
micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(5) %>
created_at: <%= 42.days.ago %>
user: michael
<% end %>
```
綜上,修改后的微博固件如[代碼清單 11.26](#listing-updated-micropost-fixtures) 所示。
##### 代碼清單 11.26:添加關聯用戶后的微博固件
test/fixtures/microposts.yml
```
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
tau_manifesto:
content: "Check out the @tauday site by @mhartl: http://tauday.com"
created_at: <%= 3.years.ago %>
user: michael
cat_video:
content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
created_at: <%= 2.hours.ago %>
user: michael
most_recent:
content: "Writing a short test"
created_at: <%= Time.zone.now %>
user: michael
<% 30.times do |n| %> micropost_<%= n %>:
content: <%= Faker::Lorem.sentence(5) %> created_at: <%= 42.days.ago %> user: michael <% end %>
```
測試數據準備好了,測試本身也很簡單:訪問資料頁面,檢查頁面的標題、用戶的名字、Gravatar 頭像、微博數量和分頁顯示的微博,如[代碼清單 11.27](#listing-user-profile-test) 所示。注意,為了使用[代碼清單 4.2](chapter4.html#listing-title-helper) 中的 `full_title` 輔助方法測試頁面的標題,我們要把 `ApplicationHelper` 模塊引入測試。[[5](#fn-5)]
##### 代碼清單 11.27:用戶資料頁面的測試 GREEN
test/integration/users_profile_test.rb
```
require 'test_helper'
class UsersProfileTest < ActionDispatch::IntegrationTest
include ApplicationHelper
def setup
@user = users(:michael)
end
test "profile display" do
get user_path(@user)
assert_template 'users/show'
assert_select 'title', full_title(@user.name)
assert_select 'h1', text: @user.name
assert_select 'h1>img.gravatar'
assert_match @user.microposts.count.to_s, response.body
assert_select 'div.pagination'
@user.microposts.paginate(page: 1).each do |micropost|
assert_match micropost.content, response.body
end
end
end
```
檢查微博數量時用到了 `response.body`,[第 10 章的練習](chapter10.html#account-activation-and-password-reset-exercises)中見過。別被名字迷惑了,其實 `response.body` 的值是整個頁面的 HTML 源碼(不只是 `body` 元素中的內容)。如果我們只關心頁面中某處顯示的微博數量,使用下面的斷言找到匹配的內容即可:
```
assert_match @user.microposts.count.to_s, response.body
```
`assert_match` 沒有 `assert_select` 的針對性強,無需指定要查找哪個 HTML 標簽。
[代碼清單 11.27](#listing-user-profile-test) 還在 `assert_select` 中使用了嵌套式句法:
```
assert_select 'h1>img.gravatar'
```
這行代碼的意思是,在 `h1` 標簽中查找類為 `gravatar` 的 `img` 標簽。
因為應用能正常運行,所以測試組件應該也能通過:
##### 代碼清單 11.28:**GREEN**
```
$ bundle exec rake test
```
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習