9.3 列出所有用户
最后更新于:2022-04-01 22:29:53
# 9.3 列出所有用户
本节,我们要添加倒数第二个用户控制器动作,`index`。`index` 动作不是显示某一个用户,而是显示所有用户。在这个过程中,我们要学习如何在数据库中生成示例用户数据,以及如何分页显示用户列表,让首页显示任意数量的用户。用户列表、分页链接和“Users”(所有用户)导航链接的构思图如[图 9.8](#fig-user-index-mockup) 所示。[[6](#fn-6)][9.4 节](#deleting-users)会添加管理功能,用来删除用户。
![user index mockup bootstrap](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/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') %>
```
至此,用户列表页面完成了,所有的测试也都可以通过了:
##### 代码清单 9.37:**GREEN**
```
$ bundle exec rake test
```
不过,如[图 9.9](#fig-user-index-only-one) 所示,页面中只显示了一个用户,有点孤单。下面,我们来改变这种悲惨状况。
![user index only one 3rd edition](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/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://docs.gechiui.com/gc-content/uploads/sites/kancloud/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') %>
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') %>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
```
这个改进不错,不过我们还可以做得更好。我们可以直接把 `@users` 变量传给 `render` 方法,如[代码清单 9.48](#listing-index-final-refactoring) 所示。
##### 代码清单 9.48:完全重构后的用户列表视图 GREEN
app/views/users/index.html.erb
```
<% provide(:title, 'All users') %>
';
All users
-
<% @users.each do |user| %>
- <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% end %>
<%= link_to "sample app", root_path, id: "logo" %>
All users
<%= will_paginate %>-
<% @users.each do |user| %>
- <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> <% end %>
All users
<%= will_paginate %>-
<% @users.each do |user| %>
<%= render user %>
<% end %>
All users
<%= will_paginate %>-
<%= render @users %>