# 9.1 更新用戶
編輯用戶信息的方法和創建新用戶差不多(參見[第 7 章](chapter7.html#sign-up)),創建新用戶的頁面在 `new` 動作中處理,而編輯用戶的頁面在 `edit` 動作中處理;創建用戶的過程在 `create` 動作中處理 `POST` 請求,編輯用戶要在 `update` 動作中處理 `PATCH` 請求([旁注 3.2](chapter3.html#aside-get-etc))。二者之間最大的區別是,任何人都可以注冊,但只有當前用戶才能更新自己的信息。我們可以使用[第 8 章](chapter8.html#log-in-log-out)實現的認證機制,通過“事前過濾器”(before filter)實現訪問限制。
開始實現之前,我們先切換到 `updating-users` 主題分支:
```
$ git checkout master
$ git checkout -b updating-users
```
## 9.1.1 編輯表單
我們先來創建編輯表單,構思圖如[圖 9.1](#fig-edit-user-mockup)。[[1](#fn-1)]要把這個構思圖轉換成可以使用的頁面,我們既要編寫用戶控制器的 `edit` 動作,也要創建編輯用戶的視圖。我們先來編寫 `edit` 動作。在 `edit` 動作中我們要從數據庫中讀取相應的用戶。由[表 7.1](chapter7.html#table-restful-users) 得知,用戶的編輯頁面地址是 /users/1/edit(假設用戶的 ID 是 1)。我們知道用戶的 ID 可以使用 `params[:id]` 獲取,那么就可以使用[代碼清單 9.1](#listing-initial-edit-action) 中的代碼查找用戶。
##### 代碼清單 9.1:用戶控制器的 `edit` 動作
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
def edit
@user = User.find(params[:id]) end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
```
圖 9.1:用戶編輯頁面的構思圖
用戶編輯頁面的視圖(要手動創建這個文件)如[代碼清單 9.2](#listing-user-edit-view) 所示。注意,這個視圖和[代碼清單 7.13](chapter7.html#listing-signup-form) 中新建用戶的視圖很相似,有很多重復的代碼,所以可以重構,把共用的代碼放到局部視圖中,這個任務留作練習([9.6 節](#updating-showing-and-deleting-users-exercises))。
##### 代碼清單 9.2:用戶編輯頁面的視圖
app/views/users/edit.html.erb
```
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">change</a>
</div>
</div>
</div>
```
這里再次用到了 [7.3.3 節](chapter7.html#signup-error-messages)創建的 `error_messages` 局部視圖。順便說一下,修改 Gravatar 頭像的鏈接用到了 `target="_blank"`,目的是在新窗口或選項卡中打開這個網頁。鏈接到第三方網站時一般都會這么做。
[代碼清單 9.1](#listing-initial-edit-action) 中定義了 `@user` 實例變量,所以編輯頁面可以正確渲染,如[圖 9.2](#fig-edit-page) 所示。從“Name”和“Email”字段可以看出,Rails 會自動使用 `@user` 變量的屬性值填寫相應的字段。
圖 9.2:編輯頁面初始版本,名字和電子郵件地址自動填入了值
查看用戶編輯頁面的 HTML 源碼,會看到預期的表單標簽,如[代碼清單 9.3](#listing-edit-form-html) 所示(某些細節可能不同)。
##### 代碼清單 9.3:[代碼清單 9.2](#listing-user-edit-view) 定義的編輯表單生成的 HTML
```
<form accept-charset="UTF-8" action="/users/1" class="edit_user"
id="edit_user_1" method="post">
<input name="_method" type="hidden" value="patch" />
.
.
.
</form>
```
留意一下這個隱藏字段:
```
<input name="_method" type="hidden" value="patch" />
```
因為瀏覽器并不支持發送 `PATCH` 請求([表 7.1](chapter7.html#table-restful-users) 中的 REST 動作要用),所以 Rails 在 `POST` 請求中使用這個隱藏字段偽造了一個 `PATCH` 請求。[[2](#fn-2)]
還有一個細節需要注意一下,[代碼清單 9.2](#listing-user-edit-view) 和[代碼清單 7.13](chapter7.html#listing-signup-form) 都使用了相同的 `form_for(@user)` 來構建表單,那么 Rails 是怎么知道創建新用戶要發送 `POST` 請求,而編輯用戶時要發送 `PATCH` 請求的呢?這個問題的答案是,通過 Active Record 提供的 `new_record?` 方法檢測用戶是新創建的還是已經存在于數據庫中:
```
$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false
```
所以使用 `form_for(@user)` 構建表單時,如果 `@user.new_record?` 返回 `true`,發送 `POST` 請求,否則發送 `PATCH` 請求。
最后,我們要把導航中指向編輯用戶頁面的鏈接換成真實的地址。很簡單,我們直接使用[表 7.1](chapter7.html#table-restful-users) 中列出的 `edit_user_path` 具名路由,并把參數設為[代碼清單 8.36](chapter8.html#listing-persistent-current-user) 中定義的 `current_user` 輔助方法:
```
<%= link_to "Settings", edit_user_path(current_user) %>
```
完整的視圖如[代碼清單 9.4](#listing-settings-link) 所示。
##### 代碼清單 9.4:在網站布局中設置“Settings”鏈接的地址
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", '#' %></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.1.2 編輯失敗
本節我們要處理編輯失敗的情況,過程和處理注冊失敗差不多([7.3 節](chapter7.html#unsuccessful-signups))。我們要先定義 `update` 動作,把提交的 `params` 哈希傳給 `update_attributes` 方法([6.1.5 節](chapter6.html#updating-user-objects)),更新用戶,如[代碼清單 9.5](#listing-user-update-action-unsuccessful) 所示。如果提交的數據無效,更新操作會返回 `false`,由 `else` 分支處理,重新渲染編輯頁面。我們之前用過類似的處理方式,代碼結構和第一個版本的 `create` 動作類似([代碼清單 7.16](chapter7.html#listing-first-create-action))。
##### 代碼清單 9.5:`update` 動作初始版本
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new' end
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params) # 處理更新成功的情況
else
render 'edit' end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
```
注意在調用 `update_attributes` 方法時指定的 `user_params` 參數,這種用法是“健壯參數”(strong parameter),可以避免批量賦值帶來的安全隱患(參見 [7.3.2 節](chapter7.html#strong-parameters))。
因為用戶模型中定義了驗證規則,而且[代碼清單 9.2](#listing-user-edit-view) 中渲染了錯誤消息局部視圖,所以提交無效信息后會顯示一些有用的錯誤消息,如[圖 9.3](#fig-buggy-edit-with-invalid-information) 所示。
圖 9.3:提交編輯表單后顯示的錯誤消息
## 9.1.3 編輯失敗的測試
[9.1.2 節](#unsuccessful-edits)結束時編輯表單已經可以使用,按照[旁注 3.3](chapter3.html#aside-when-to-test) 中的測試指導方針,現在我們要編寫集成測試捕獲回歸。和之前一樣,首先要生成一個集成測試文件:
```
$ rails generate integration_test users_edit
invoke test_unit
create test/integration/users_edit_test.rb
```
然后為編輯失敗編寫一個簡單的測試,如[代碼清單 9.6](#listing-unsuccessful-edit-test) 所示。在這段測試中,我們檢查提交無效信息后會重新渲染編輯模板,以此確認表現是否正確。注意,這里使用 `patch` 方法發起 `PATCH` 請求,用法與 `get`、`post` 和 `delete` 類似。
##### 代碼清單 9.6:編輯失敗的測試 GREEN
test/integration/users_edit_test.rb
```
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user)
patch user_path(@user), user: { name: '',
email: 'foo@invalid',
password: 'foo',
password_confirmation: 'bar' }
assert_template 'users/edit'
end
end
```
此時,測試組件應該可以通過:
##### 代碼清單 9.7:**GREEN**
```
$ bundle exec rake test
```
## 9.1.4 編輯成功(使用 TDD)
現在我們要讓編輯表單能正常使用。編輯頭像的功能已經有了,因為我們把上傳頭像的操作交由 Gravatar 處理,如需更換頭像,點擊[圖 9.2](#fig-edit-page) 中的“change”鏈接就可以了,如[圖 9.4](#fig-gravatar-cropper) 所示。下面我們來實現編輯其他信息的功能。
圖 9.4:Gravatar 的圖片剪切界面,上傳了一個[帥哥](http://www.michaelhartl.com/)的圖片
上手測試后,你可能會發現,編寫應用代碼之前編寫測試比之后再寫更有用。針對現在這種情況,我們要編寫的是“驗收測試”(acceptance test),由測試的結果決定某個功能是否完成。為了演示如何編寫驗收測試,我們要使用測試驅動開發技術完成用戶編輯功能。
我們要編寫類似[代碼清單 9.6](#listing-unsuccessful-edit-test) 中的測試,確認更新用戶的操作表現正確,只不過這一次我們會提交有效的信息。然后檢查顯示了閃現消息,而且成功重定向到了用戶的資料頁面,同時還要確認數據庫中保存的用戶信息也正確更新了。這個測試如[代碼清單 9.8](#listing-successful-edit-test) 所示。注意,在[代碼清單 9.8](#listing-successful-edit-test) 中,密碼和密碼確認都為空值,因為修改用戶名和電子郵件地址時并不想修改密碼。還要注意,我們使用 `@user.reload`([6.1.5 節](chapter6.html#updating-user-objects)首次用到)重新加載數據庫中存儲的值,以此確認成功更新了信息。(新手很容易忘記這個操作,這就是為什么必須要有一定的經驗才能編寫有效的驗收測試(推及到 TDD)的原因。)
##### 代碼清單 9.8:編輯成功的測試 RED
test/integration/users_edit_test.rb
```
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit" do
get edit_user_path(@user)
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), user: { name: name,
email: email,
password: "",
password_confirmation: "" }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal @user.name, name
assert_equal @user.email, email
end
end
```
要讓[代碼清單 9.8](#listing-successful-edit-test) 中的測試通過,我們可以參照最終版 `create` 動作([代碼清單 8.22](chapter8.html#listing-login-upon-signup))來編寫 `update` 動作,如[代碼清單 9.9](#listing-user-update-action) 所示。
##### 代碼清單 9.9:用戶控制器的 `update` 動作 RED
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
.
.
.
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = "Profile updated" redirect_to @user else
render 'edit'
end
end
.
.
.
end
```
如[代碼清單 9.9](#listing-user-update-action) 的標題所示,測試組件無法通過,因為密碼長度驗證([代碼清單 6.39](chapter6.html#listing-password-implementation))失敗了,這是因為[代碼清單 9.8](#listing-successful-edit-test) 中密碼和密碼確認都是空值。為了讓測試通過,我們要在密碼為空值時特殊處理最短長度驗證,方法是把 `allow_nil: true` 參數傳給 `validates` 方法,如[代碼清單 9.10](#listing-allow-blank-password) 所示。
##### 代碼清單 9.10:更新時允許密碼為空 GREEN
app/models/user.rb
```
class User < ActiveRecord::Base
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 }
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true .
.
.
end
```
你可能擔心這么改用戶注冊時可以把密碼設為空值,其實不然,[6.3.3 節](chapter6.html#minimum-password-standards)說過,創建對象時,`has_secure_password` 會執行存在性驗證,捕獲密碼為 `nil` 的情況。(密碼為 `nil` 時能通過存在性驗證,可是會被 `has_secure_password` 方法的驗證捕獲,因此修正了 [7.3.3 節](chapter7.html#signup-error-messages)提到的錯誤消息重復問題。)
至此,用戶編輯頁面應該可以正常使用了,如[圖 9.5](#fig-edit-form-working) 所示。你也可以運行測試組件確認一下,應該可以通過:
##### 代碼清單 9.11:**GREEN**
```
$ bundle exec rake test
```
圖 9.5:編輯成功后顯示的頁面
- 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 練習