# 9.3 列出所有用户
本节,我们要添加倒数第二个用户控制器动作,`index`。`index` 动作不是显示某一个用户,而是显示所有用户。在这个过程中,我们要学习如何在数据库中生成示例用户数据,以及如何分页显示用户列表,让首页显示任意数量的用户。用户列表、分页链接和“Users”(所有用户)导航链接的构思图如[图 9.8](#fig-user-index-mockup) 所示。[[6](#fn-6)][9.4 节](#deleting-users)会添加管理功能,用来删除用户。
![user index mockup bootstrap](https://box.kancloud.cn/2016-05-11_5733305f028fd.png)图 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) 所示,页面中只显示了一个用户,有点孤单。下面,我们来改变这种悲惨状况。
![user index only one 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f178f7.png)图 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 头像了。
![user index all 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f32963.png)图 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` 方法,所以这两个地方都会显示分页链接。
![user index pagination 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f4a488.png)图 9.11:分页显示的用户列表页面
如果点击链接“2”,或者“Next”,就会显示第二页,如[图 9.12](#fig-user-index-page-two-rails-3) 所示。
![user index page two 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f6c2f7.png)图 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 练习