# 9.3 列出所有用戶
本節,我們要添加倒數第二個用戶控制器動作,`index`。`index` 動作不是顯示某一個用戶,而是顯示所有用戶。在這個過程中,我們要學習如何在數據庫中生成示例用戶數據,以及如何分頁顯示用戶列表,讓首頁顯示任意數量的用戶。用戶列表、分頁鏈接和“Users”(所有用戶)導航鏈接的構思圖如[圖 9.8](#fig-user-index-mockup) 所示。[[6](#fn-6)][9.4 節](#deleting-users)會添加管理功能,用來刪除用戶。
圖 9.8:用戶列表頁面的構思圖
## 9.3.1 用戶列表
創建用戶列表之前,我們先要實現一個安全機制。單個用戶的資料頁面對網站的所有訪問者開放,但要限制用戶列表頁面,只讓已登錄的用戶查看,減少未注冊用戶能看到的信息量。[[7](#fn-7)]
為了限制訪問 `index` 動作,我們先編寫一個簡短的測試,確認應用會正確重定向 `index` 動作,如[代碼清單 9.31](#listing-index-action-redirected-test) 所示。
##### 代碼清單 9.31:測試 `index` 動作的重定向 RED
test/controllers/users_controller_test.rb
```
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
def setup
@user = users(:michael)
@other_user = users(:archer)
end
test "should redirect index when not logged in" do get :index assert_redirected_to login_url end .
.
.
end
```
然后我們要定義 `index` 動作,并把它加入被 `logged_in_user` 事前過濾器保護的動作列表中,如[代碼清單 9.32](#listing-logged-in-user-index) 所示。
##### 代碼清單 9.32:訪問 `index` 動作要先登錄 GREEN
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update]
def index end
def show
@user = User.find(params[:id])
end
.
.
.
end
```
若要顯示用戶列表,我們要定義一個變量,存儲網站中的所有用戶,然后在 `index` 動作的視圖中遍歷,顯示各個用戶。你可能還記得玩具應用中相應的動作([2.5 節](chapter2.html#a-toy-app-exercises)),我們可以使用 `User.all` 從數據庫中讀取所有用戶,然后把這些用戶賦值給實例變量 `@users`,以便在視圖中使用,如[代碼清單 9.33](#listing-user-index) 所示。(你可能會覺得一次列出所有用戶不太好,你是對的,我們會在 [9.3.3 節](#pagination)改進。)
##### 代碼清單 9.33:用戶控制器的 `index` 動作
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.all end
.
.
.
end
```
為了顯示用戶列表頁面,我們要創建一個視圖(要自己動手創建視圖文件),遍歷所有用戶,把每個用戶包含在一個 `li` 標簽中。我們要使用 `each` 方法遍歷所有用戶,顯示用戶的 Gravatar 頭像和名字,然后把所有用戶包含在一個無序列表 `ul` 標簽中,如[代碼清單 9.34](#listing-user-index-view) 所示。
##### 代碼清單 9.34:`index` 視圖
app/views/users/index.html.erb
```
<% provide(:title, 'All users') %>
<h1>All users</h1>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
```
在[代碼清單 9.34](#listing-user-index-view) 中,我們用到了 [7.7 節](chapter7.html#sign-up-exercises)練習中[代碼清單 7.31](chapter7.html#listing-gravatar-option) 的成果,向 Gravatar 輔助方法傳入第二個參數,指定頭像的大小。如果你之前沒有做這個練習,在繼續閱讀之前請參照[代碼清單 7.31](chapter7.html#listing-gravatar-option),更新用戶控制器的輔助方法文件。
然后再添加一些 CSS 樣式(確切地說是 SCSS),如[代碼清單 9.35](#listing-user-index-css)。
##### 代碼清單 9.35:用戶列表頁面的 CSS
app/assets/stylesheets/custom.css.scss
```
.
.
.
/* Users index */
.users {
list-style: none;
margin: 0;
li {
overflow: auto;
padding: 10px 0;
border-bottom: 1px solid $gray-lighter;
}
}
```
最后,我們還要把頭部導航中用戶列表頁面的鏈接地址換成 `users_path`,這是[表 7.1](chapter7.html#table-restful-users) 中還沒用到的最后一個具名路由,如[代碼清單 9.36](#listing-users-link) 所示。
##### 代碼清單 9.36:添加用戶列表頁面的鏈接地址
app/views/layouts/_header.html.erb
```
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", users_path %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: "delete" %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
```
至此,用戶列表頁面完成了,所有的測試也都可以通過了:
##### 代碼清單 9.37:**GREEN**
```
$ bundle exec rake test
```
不過,如[圖 9.9](#fig-user-index-only-one) 所示,頁面中只顯示了一個用戶,有點孤單。下面,我們來改變這種悲慘狀況。
圖 9.9:用戶列表頁面,只顯示了一個用戶
## 9.3.2 示例用戶
本節,我們要為應用添加更多的用戶。為了讓用戶列表看上去像個“列表”,我們可以在瀏覽器中訪問注冊頁面,一個一個地注冊用戶,不過還有更好的方法,讓 Ruby(和 Rake)為我們創建用戶。
首先,我們要在 `Gemfile` 中加入 `faker` gem,如[代碼清單 9.38](#listing-faker-gemfile) 所示。這個 gem 會使用半真實的名字和電子郵件地址創建示例用戶。(通常,可能只需在開發環境中安裝 `faker` gem,但是對這個演示應用來說,生產環境也要使用 `faker`,參見 [9.5 節](#updating-showing-and-deleting-users-conclusion)。)
##### 代碼清單 9.38:在 `Gemfile` 中加入 `faker`
```
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2' .
.
.
```
然后和之前一樣,運行下面的命令安裝:
```
$ bundle install
```
接下來,我們要添加一個 Rake 任務,向數據庫中添加示例用戶。Rails 使用一個標準文件 `db/seeds.rb` 完成這種操作,如[代碼清單 9.39](#listing-db-seed) 所示。(這段代碼涉及一些高級知識,現在不必太關注細節。)
##### 代碼清單 9.39:向數據庫中添加示例用戶的 Rake 任務
db/seeds.rb
```
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password)
end
```
在[代碼清單 9.39](#listing-db-seed) 中,首先使用現有用戶的名字和電子郵件地址創建一個示例用戶,然后又創建了 99 個示例用戶。其中,`create!` 方法和 `create` 方法的作用類似,只不過遇到無效數據時會拋出異常,而不是返回 `false`。這么做出現錯誤時不會靜默,有利于調試。
然后,我們可以執行下述命令,還原數據庫,再使用 `db:seed` 調用這個 Rake 任務:[[8](#fn-8)]
```
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
```
向數據庫中添加數據的操作可能很慢,在某些系統中可能要花上幾分鐘。此外,有些讀者反饋說,Rails 服務器運行的過程中無法執行 `reset` 命令,因此,可能要先停止服務器,然后再執行上述命令。
執行完 `db:seed` Rake 任務后,我們的應用中就有 100 個用戶了,如[圖 9.10](#fig-user-index-all) 所示。(可能要重啟服務器才能看到效果。)我犧牲了一點個人時間,為前幾個用戶上傳了頭像,這樣就不會都顯示默認的 Gravatar 頭像了。
圖 9.10:用戶列表頁面,顯示了 100 個示例用戶
## 9.3.3 分頁
現在,最初的那個用戶不再孤單了,但是又出現了新問題:用戶太多,全在一個頁面中顯示。現在的用戶數量是 100 個,算是少的了,在真實的網站中,這個數量可能是以千計的。為了避免在一頁中顯示過多的用戶,我們可以分頁,一頁只顯示 30 個用戶。
在 Rails 中有很多實現分頁的方法,我們要使用其中一個最簡單也最完善的,叫 [will_paginate](http://wiki.github.com/mislav/will_paginate/)。為此,我們要使用 `will_paginate` 和 `bootstrap-will_paginate` 這兩個 gem。其中,`bootstrap-will_paginate` 的作用是設置 will_paginate 使用 Bootstrap 提供的分頁樣式。修改后的 `Gemfile` 如[代碼清單 9.40](#listing-will-paginate-gem) 所示。
##### 代碼清單 9.40:在 `Gemfile` 中加入 `will_paginate`
```
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
gem 'faker', '1.4.2'
gem 'will_paginate', '3.0.7' gem 'bootstrap-will_paginate', '0.0.10' .
.
.
```
然后執行下面的命令安裝:
```
$ bundle install
```
安裝后還要重啟 Web 服務器,確保成功加載這兩個新 gem。
為了實現分頁,我們要在 `index` 視圖中加入一些代碼,告訴 Rails 分頁顯示用戶,而且要把 `index` 動作中的 `User.all` 換成知道如何分頁的方法。我們先在視圖中加入特殊的 `will_paginate` 方法,如[代碼清單 9.41](#listing-will-paginate-index-view) 所示。稍后我們會看到為什么要在用戶列表的前后都加入這個方法。
##### 代碼清單 9.41:在 `index` 視圖中加入分頁
app/views/users/index.html.erb
```
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
```
`will_paginate` 方法有點小神奇,在用戶控制器的視圖中,它會自動尋找名為 `@users` 的對象,然后顯示一個分頁導航鏈接。[代碼清單 9.41](#listing-will-paginate-index-view) 中的視圖現在還不能正確顯示分頁,因為 `@users` 的值是通過 `User.all` 方法獲取的([代碼清單 9.33](#listing-user-index)),而 `will_paginate` 需要調用 `paginate` 方法才能分頁:
```
$ rails console
>> User.paginate(page: 1)
User Load (1.5ms) SELECT "users".* FROM "users" LIMIT 30 OFFSET 0
(1.7ms) SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
```
注意,`paginate` 方法可以接受一個哈希參數,`:page` 鍵的值指定顯示第幾頁。`User.paginate` 方法根據 `:page` 的值,一次取回一組用戶(默認為 30 個)。所以,第一頁顯示的是第 1-30 個用戶,第二頁顯示的是第 31-60 個,以此類推。如果 `:page` 的值為 `nil`,`paginate` 會顯示第一頁。
我們可以把 `index` 動作中的 `all` 方法換成 `paginate`,如[代碼清單 9.42](#listing-will-paginate-index-action) 所示,這樣就能分頁顯示用戶了。`paginate` 方法所需的 `:page` 參數由 `params[:page]` 指定,`params` 中的這個鍵由 `will_pagenate` 自動生成。
##### 代碼清單 9.42:在 `index` 動作中分頁取回用戶
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
.
def index
@users = User.paginate(page: params[:page]) end
.
.
.
end
```
現在,用戶列表頁面應該可以顯示分頁了,如[圖 9.11](#fig-user-index-pagination) 所示。(在某些系統中,可能需要重啟 Rails 服務器。)因為我們在用戶列表前后都加入了 `will_paginate` 方法,所以這兩個地方都會顯示分頁鏈接。
圖 9.11:分頁顯示的用戶列表頁面
如果點擊鏈接“2”,或者“Next”,就會顯示第二頁,如[圖 9.12](#fig-user-index-page-two-rails-3) 所示。
圖 9.12:用戶列表的第二頁
## 9.3.4 用戶列表頁面的測試
現在用戶列表頁面可以正常使用了,接下來要為這個頁面編寫一些簡單的測試,其中一個測試前一節實現的分頁。測試的步驟是,先登錄,然后訪問用戶列表頁面,確認第一頁顯示了一些用戶,而且還顯示了分頁鏈接。為此,測試數據庫中要有能足夠數量的用戶,足以分頁才行,即超過 30 個。
我們在[代碼清單 9.20](#listing-fixture-second-user) 中創建了第二個用戶固件,但手動創建 30 多個用戶,工作量有點大。不過,由固件中的 `password_digest` 屬性得知,固件文件支持嵌入式 Ruby,所以我們可以使用[代碼清單 9.43](#listing-users-fixtures-extra-users) 中的代碼,再創建 30 個用戶。([代碼清單 9.43](#listing-users-fixtures-extra-users) 還多創建了幾個用戶,以備后用。)
##### 代碼清單 9.43:在固件中再創建 30 個用戶
test/fixtures/users.yml
```
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
malory:
name: Malory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
```
然后,我們可以編寫用戶列表頁面的測試了。首先,生成所需的測試文件:
```
$ rails generate integration_test users_index
invoke test_unit
create test/integration/users_index_test.rb
```
在測試中,我們要檢查是否有一個類為 `pagination` 的標簽,以及第一頁中是否顯示了用戶,如[代碼清單 9.44](#listing-user-index-test) 所示。
##### 代碼清單 9.44:用戶列表及分頁的測試 GREEN
test/integration/users_index_test.rb
```
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select 'div.pagination'
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end
```
測試組件應該可以通過:
##### 代碼清單 9.45:**GREEN**
```
$ bundle exec rake test
```
## 9.3.5 使用局部視圖重構
用戶列表頁面現在已經可以顯示分頁了,但是有個地方可以改進,我不得不介紹一下。Rails 提供了一些很巧妙的方法,可以精簡視圖的結構。本節我們要利用這些方法重構一下用戶列表頁面。因為我們已經做了很好的測試,所以可以放心重構,不必擔心會破壞網站的功能。
重構的第一步,把[代碼清單 9.41](#listing-will-paginate-index-view) 中的 `li` 換成 `render` 方法調用,如[代碼清單 9.46](#listing-index-view-first-refactoring) 所示。
##### 代碼清單 9.46:重構用戶列表視圖的第一步
app/views/users/index.html.erb
```
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
```
在上述代碼中,`render` 的參數不再是指定局部視圖的字符串,而是代表 `User` 類的變量 `user`。[[9](#fn-9)]此時,Rails 會自定尋找一個名為 `_user.html.erb` 的局部視圖。我們要手動創建這個視圖,然后寫入[代碼清單 9.47](#listing-user-partial) 中的內容。
##### 代碼清單 9.47:顯示單個用戶的局部視圖
app/views/users/_user.html.erb
```
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
```
這個改進不錯,不過我們還可以做得更好。我們可以直接把 `@users` 變量傳給 `render` 方法,如[代碼清單 9.48](#listing-index-final-refactoring) 所示。
##### 代碼清單 9.48:完全重構后的用戶列表視圖 GREEN
app/views/users/index.html.erb
```
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %> </ul>
<%= will_paginate %>
```
Rails 會把 `@users` 當作一個 `User` 對象列表,傳給 `render` 方法后,Rails 會自動遍歷這個列表,然后使用局部視圖 `_user.html.erb` 渲染每個對象。重構后,我們得到了如[代碼清單 9.48](#listing-index-final-refactoring) 這樣簡潔的代碼。
每次重構修改應用代碼后,都要運行測試組件確認仍能通過:
##### 代碼清單 9.49:**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 練習