9.3 列出所有用户
本节,我们要添加倒数第二个用户控制器动作,index
。index
动作不是显示某一个用户,而是显示所有用户。在这个过程中,我们要学习如何在数据库中生成示例用户数据,以及如何分页显示用户列表,让首页显示任意数量的用户。用户列表、分页链接和“Users”(所有用户)导航链接的构思图如图 9.8 所示。[6]9.4 节会添加管理功能,用来删除用户。
图 9.8:用户列表页面的构思图
9.3.1 用户列表
创建用户列表之前,我们先要实现一个安全机制。单个用户的资料页面对网站的所有访问者开放,但要限制用户列表页面,只让已登录的用户查看,减少未注册用户能看到的信息量。[7]
为了限制访问 index
动作,我们先编写一个简短的测试,确认应用会正确重定向 index
动作,如代码清单 9.31 所示。
代码清单 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 所示。
代码清单 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 节),我们可以使用 User.all
从数据库中读取所有用户,然后把这些用户赋值给实例变量 @users
,以便在视图中使用,如代码清单 9.33 所示。(你可能会觉得一次列出所有用户不太好,你是对的,我们会在 9.3.3 节改进。)
代码清单 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 所示。
代码清单 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 中,我们用到了 7.7 节练习中代码清单 7.31 的成果,向 Gravatar 辅助方法传入第二个参数,指定头像的大小。如果你之前没有做这个练习,在继续阅读之前请参照代码清单 7.31,更新用户控制器的辅助方法文件。
然后再添加一些 CSS 样式(确切地说是 SCSS),如代码清单 9.35。
代码清单 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 中还没用到的最后一个具名路由,如代码清单 9.36 所示。
代码清单 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 所示,页面中只显示了一个用户,有点孤单。下面,我们来改变这种悲惨状况。
图 9.9:用户列表页面,只显示了一个用户
9.3.2 示例用户
本节,我们要为应用添加更多的用户。为了让用户列表看上去像个“列表”,我们可以在浏览器中访问注册页面,一个一个地注册用户,不过还有更好的方法,让 Ruby(和 Rake)为我们创建用户。
首先,我们要在 Gemfile
中加入 faker
gem,如代码清单 9.38 所示。这个 gem 会使用半真实的名字和电子邮件地址创建示例用户。(通常,可能只需在开发环境中安装 faker
gem,但是对这个演示应用来说,生产环境也要使用 faker
,参见 9.5 节。)
代码清单 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 所示。(这段代码涉及一些高级知识,现在不必太关注细节。)
代码清单 9.39:向数据库中添加示例用户的 Rake 任务
db/seeds.rb
User.create!(name: "Example User",
email: "[email protected]",
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 中,首先使用现有用户的名字和电子邮件地址创建一个示例用户,然后又创建了 99 个示例用户。其中,create!
方法和 create
方法的作用类似,只不过遇到无效数据时会抛出异常,而不是返回 false
。这么做出现错误时不会静默,有利于调试。
然后,我们可以执行下述命令,还原数据库,再使用 db:seed
调用这个 Rake 任务:[8]
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
向数据库中添加数据的操作可能很慢,在某些系统中可能要花上几分钟。此外,有些读者反馈说,Rails 服务器运行的过程中无法执行 reset
命令,因此,可能要先停止服务器,然后再执行上述命令。
执行完 db:seed
Rake 任务后,我们的应用中就有 100 个用户了,如图 9.10 所示。(可能要重启服务器才能看到效果。)我牺牲了一点个人时间,为前几个用户上传了头像,这样就不会都显示默认的 Gravatar 头像了。
图 9.10:用户列表页面,显示了 100 个示例用户
9.3.3 分页
现在,最初的那个用户不再孤单了,但是又出现了新问题:用户太多,全在一个页面中显示。现在的用户数量是 100 个,算是少的了,在真实的网站中,这个数量可能是以千计的。为了避免在一页中显示过多的用户,我们可以分页,一页只显示 30 个用户。
在 Rails 中有很多实现分页的方法,我们要使用其中一个最简单也最完善的,叫 will_paginate。为此,我们要使用 will_paginate
和 bootstrap-will_paginate
这两个 gem。其中,bootstrap-will_paginate
的作用是设置 will_paginate 使用 Bootstrap 提供的分页样式。修改后的 Gemfile
如代码清单 9.40 所示。
代码清单 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 所示。稍后我们会看到为什么要在用户列表的前后都加入这个方法。
代码清单 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 中的视图现在还不能正确显示分页,因为 @users
的值是通过 User.all
方法获取的(代码清单 9.33),而 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 所示,这样就能分页显示用户了。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 所示。(在某些系统中,可能需要重启 Rails 服务器。)因为我们在用户列表前后都加入了 will_paginate
方法,所以这两个地方都会显示分页链接。
图 9.11:分页显示的用户列表页面
如果点击链接“2”,或者“Next”,就会显示第二页,如图 9.12 所示。
图 9.12:用户列表的第二页
9.3.4 用户列表页面的测试
现在用户列表页面可以正常使用了,接下来要为这个页面编写一些简单的测试,其中一个测试前一节实现的分页。测试的步骤是,先登录,然后访问用户列表页面,确认第一页显示了一些用户,而且还显示了分页链接。为此,测试数据库中要有能足够数量的用户,足以分页才行,即超过 30 个。
我们在代码清单 9.20 中创建了第二个用户固件,但手动创建 30 多个用户,工作量有点大。不过,由固件中的 password_digest
属性得知,固件文件支持嵌入式 Ruby,所以我们可以使用代码清单 9.43 中的代码,再创建 30 个用户。(代码清单 9.43 还多创建了几个用户,以备后用。)
代码清单 9.43:在固件中再创建 30 个用户
test/fixtures/users.yml
michael:
name: Michael Example
email: [email protected]
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: [email protected]
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: [email protected]
password_digest: <%= User.digest('password') %>
malory:
name: Malory Archer
email: [email protected]
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 所示。
代码清单 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 中的 li
换成 render
方法调用,如代码清单 9.46 所示。
代码清单 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]此时,Rails 会自定寻找一个名为 _user.html.erb
的局部视图。我们要手动创建这个视图,然后写入代码清单 9.47 中的内容。
代码清单 9.47:显示单个用户的局部视图
app/views/users/_user.html.erb
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
这个改进不错,不过我们还可以做得更好。我们可以直接把 @users
变量传给 render
方法,如代码清单 9.48 所示。
代码清单 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 这样简洁的代码。
每次重构修改应用代码后,都要运行测试组件确认仍能通过:
代码清单 9.49:GREEN
$ bundle exec rake test