Python计算机视觉:第九章 图像分割
第九章 圖像分割
圖像分割是將一幅圖像分割成有意義區域的過程。區域可以是圖像的前景與背景或者單個對象。這些區域可以利用諸如顏色、邊線或近鄰相似性等特征構建。本章中,我們將看到一些不同的分割技術。
from pygraph.classes.digraph import digraph from pygraph.algorithms.minmax import maximum_flowgr = digraph() gr.add_nodes([0,1,2,3]) gr.add_edge((0,1), wt=4) gr.add_edge((1,2), wt=3) gr.add_edge((2,3), wt=5) gr.add_edge((0,2), wt=3) gr.add_edge((1,3), wt=4) flows,cuts = maximum_flow(gr,0,3) print 'flow is:', flows print 'cut is:', cuts flow is: {(0, 1): 4, (1, 2): 0, (1, 3): 4, (2, 3): 3, (0, 2): 3} cut is: {0: 0, 1: 1, 2: 1, 3: 1} from scipy.misc import imresize from PCV.tools import graphcut from PIL import Image from pylab import *im = array(Image.open("../data/empire.jpg")) im = imresize(im, 0.07) size = im.shape[:2]# add two rectangular training regions labels = zeros(size) labels[3:18, 3:18] = -1 labels[-18:-3, -18:-3] = 1# create graph g = graphcut.build_bayes_graph(im, labels, kappa=1)# cut the graph res = graphcut.cut_graph(g, size)figure() graphcut.show_labeling(im, labels)figure() imshow(res) gray() axis('off') show()P203
from PCV.tools import rof from pylab import * from PIL import Image import scipy.misc#im = array(Image.open('../data/ceramic-houses_t0.png').convert("L")) im = array(Image.open('../data/flower32_t0.png').convert("L")) figure() gray() subplot(131) axis('off') imshow(im)U, T = rof.denoise(im, im, tolerance=0.001) subplot(132) axis('off') imshow(U)#t = 0.4 # ceramic-houses_t0 threshold t = 0.8 # flower32_t0 threshold seg_im = U < t*U.max() #scipy.misc.imsave('ceramic-houses_t0_result.pdf', seg_im) scipy.misc.imsave('flower32_t0_result.pdf', seg_im) subplot(133) axis('off') imshow(seg_im)show()本章我們要完成表格 7.1所示的Users 資源,添加?edit、update、index?和?destroy?動作。首先我們要實現更新用戶個人資料的功能,實現這樣的功能自然要依靠安全驗證系統(基于第八章中實現的權限限制))。然后要創建一個頁面列出所有的用戶(也需要權限限制),期間會介紹示例數據和分頁功能。最后,我們還要實現刪除用戶的功能,從數據庫中刪除用戶記錄。我們不會為所有用戶都提供這種強大的權限,而是會創建管理員,授權他們來刪除用戶。
在開始之前,我們要新建?updating-users?分支:
$ git checkout -b updating-users9.1 更新用戶
編輯用戶信息的方法和創建新用戶差不多(參見第七章),創建新用戶的頁面是在?new?動作中處理的,而編輯用戶的頁面則是在?edit?動作中;創建用戶的過程是在?create?動作中處理了?POST?請求,而編輯用戶要在?update?動作中處理?PUT?請求(HTTP 請求參見旁注 3.2)。二者之間最大的區別是,任何人都可以注冊,但只有當前用戶才能更新他自己的信息。所以我們就要限制訪問,只有授權的用戶才能編輯更新資料,我們可以利用第八章實現的身份驗證機制,使用”事前過濾器(before filter)“實現訪問限制。
9.1.1 編輯表單
我們先來創建編輯表單,其構思圖如圖 9.1 所示。1和之前一樣,我們要先編寫測試。注意構思圖中修改 Gravatar 頭像的鏈接,如果你瀏覽過 Gravatar 的網站,可能就知道上傳和編輯頭像的地址是 http://gravatar.com/emails,我們就來測試編輯頁面中有沒有一個鏈接指向了這個地址。2
圖 9.1:編輯用戶頁面的構思圖
對編輯用戶表單的測試和第七章練習中的代碼 7.31 類似,同樣也測試了提交不合法數據后是否會顯示錯誤提示信息,如代碼 9.1 所示。
代碼 9.1?用戶編輯頁面的測試
spec/requests/user_pages_spec.rb
程序所需的代碼要放在?edit?動作中,我們在表格 7.1中列出了,用戶編輯頁面的地址是 /users/1/edit(假設用戶的 id 是 1)。我們介紹過用戶的 id 是保存在?params[:id]?中的,所以我們可以按照代碼 9.2 所示的方法查找用戶。
代碼 9.2?Users 控制器的?edit?方法
app/controllers/users_controller.rb
要讓測試通過,我們就要編寫編輯用戶頁面的視圖,如代碼 9.3 所示。仔細觀察一下視圖代碼,它和代碼 7.17 中創建新用戶頁面的視圖代碼很相似,這就暗示我們要進行重構,把重復的代碼移入局部視圖。重構會留作練習,詳情參見?9.6 節。
代碼 9.3?編輯用戶頁面的視圖
app/views/users/edit.html.erb
在這段代碼中我們再次使用了?7.3.2 節中創建的?error_messages?局部視圖。
添加了視圖代碼,再加上代碼 9.2 中定義的?@user?變量,代碼 9.1 中的 "編輯頁面" 測試應該就可以通過了:
$ bundle exec rspec spec/requests/user_pages_spec.rb -e "edit page"編輯用戶頁面如圖 9.2 所示,我們看到 Rails 會自動讀取?@user?變量,預先填好了名字和 Email 地址字段。
圖 9.2:編輯用戶頁面,名字和 Email 地址字段已經自動填好了
查看一下編輯用戶頁面的源碼,我們可以發現的確生成了一個?form?元素,參見代碼 9.4。
代碼 9.4?編輯表單的 HTML
<form action="/users/1" class="edit_user" id="edit_user_1" method="post"><input name="_method" type="hidden" value="put" />... </form>留意一下其中的一個隱藏字段:
<input name="_method" type="hidden" value="put" />因為瀏覽器本身并不支持發送?PUT?請求(表格 7.1中列出的 REST 動作要用),所以 Rails 就在?POST?請求中使用這個隱藏字段偽造了一個?PUT?請求。3
還有一個細節需要注意一下,代碼 9.3 和代碼 7.17 都使用了相同的?form_for(@user)?來構建表單,那么 Rails 是怎么知道創建新用戶要發送?POST?請求,而編輯用戶時要發送?PUT?請求的呢?這個問題的答案是,通過 Active Record 提供的new_record??方法可以檢測用戶是新創建的還是已經存在于數據庫中的:
$ rails console >> User.new.new_record? => true >> User.first.new_record? => false所以在使用?form_for(@user)?構建表單時,如果?@user.new_record??返回?true?則發送?POST?請求,否則就發送?PUT?請求。
最后,我們還要在導航中添加一個指向編輯用戶頁面的鏈接(“設置(Settings)”)。因為只有登錄之后才會顯示這個頁面,所以對“設置”鏈接的測試要和其他的身份驗證測試放在一起,如代碼 9.5 所示。(如果能再測試一下沒登錄時不會顯示“設置”鏈接就更完美了,這會留作練習,參見?9.6 節。)
代碼 9.5?添加檢測“設置”鏈接的測試
spec/requests/authentication_pages_spec.rb
為了簡化,代碼 9.5 中使用?sign_in?幫助方法,這個方法的作用是訪問登錄頁面,提交合法的表單數據,如代碼 9.6 所示。
代碼 9.6?用戶登錄幫助方法
spec/support/utilities.rb
如上述代碼中的注釋所說,如果沒有使用 Capybara 的話,填寫表單的操作是無效的,所以我們就添加了一行,在不使用 Capybara 時把用戶的記憶權標添加到 cookies 中:
# Sign in when not using Capybara as well. cookies[:remember_token] = user.remember_token如果直接使用 HTTP 請求方法就必須要有上面這行代碼,具體的用法在代碼 9.47 中有介紹。(注意,測試中使用的?cookies對象和真實的 cookies 對象是有點不一樣的,代碼 8.19 中使用的?cookies.permanent?方法不能在測試中使用。)你可能已經猜到了,sing_in?在后續的測試中還會用到,而且還可以用來去除重復代碼(參見?9.6 節)。
在程序中添加“設置”鏈接很簡單,我們就直接使用表格 7.1?中列出的?edit_user_path?具名路由,其參數設為代碼 8.22 中定義的?current_user?幫助方法:
<%= link_to "Settings", edit_user_path(current_user) %>完整的代碼如代碼 9.7 所示。
代碼 9.7?添加“設置”鏈接
app/views/layouts/_header.html.erb
9.1.2 編輯失敗
本小節我們要處理編輯失敗的情況,讓代碼 9.1 中對錯誤提示信息的測試通過。我們要在 Users 控制器的?update?動作中使用?update_attributes?方法,傳入提交的?params?Hash,更新用戶記錄,如代碼 9.8 所示。如果提交了不合法的數據,更新操作會返回?false,交由?else?分支處理,重新渲染編輯用戶頁面。我們之前用過類似的處理方式,代碼結構和第一個版本的?create?動作類似(參見代碼 7.21)。
代碼 9.8?還不完整的?update?動作
app/controllers/users_controller.rb
提交不合法信息后顯示了錯誤提示信息(如圖 9.3),測試就可以通過了,你可以運行測試組件驗證一下:
$ bundle exec rspec spec/圖 9.3:提交編輯表單后顯示的錯誤提示信息
9.1.3 編輯成功
現在我們要讓編輯表單能夠正常使用了。編輯頭像的功能已經實現了,因為我們把上傳頭像的操作交由 Gravatar 處理了,如需更換頭像,點擊圖 9.2 中的“change”鏈接就可以了,如圖 9.4 所示。下面我們來實現編輯其他信息的功能。
圖 9.4:Gravatar 的剪切圖片界面,上傳了一個帥哥的圖片
對?update?動作的測試和對?create?的測試類似。代碼 9.9 介紹了如何使用 Capybara 在表單中填寫合法的數據,還介紹了怎么測試提交表單的操作是否正確。測試的代碼很多,你可以參考第七章中的測試,試一下能不能完全理解。
代碼 9.9?測試 Users 控制器的?update?動作
spec/requests/user_pages_spec.rb
上述代碼中出現了一個新的方法?reload,出現在檢測用戶的屬性是否已經更新的測試中:
specify { user.reload.name.should == new_name } specify { user.reload.email.should == new_email }這兩行代碼使用?user.reload?從測試數據庫中重新加載?user?的數據,然后檢測用戶的名字和 Email 地址是否更新成了新的值。
要讓代碼 9.9 中的測試通過,我們可以參照最終版本的?create?動作(代碼 8.27)來編寫?update?動作,如代碼 9.10 所示。我們在代碼 9.8 的基礎上加入了下面這三行。
flash[:success] = "Profile updated" sign_in @user redirect_to @user注意,用戶資料更新成功之后我們再次登入了用戶,因為保存用戶時,重設了記憶權標(代碼 8.18),之前的 session 就失效了(代碼 8.22)。這也是一項安全措施,因為如果用戶更新了資料,任何會話劫持都會自動失效。
代碼 9.10?Users 控制器的?update?動作
app/controllers/users_controller.rb
注意,現在這種實現方式,每次更新數據都要提供密碼(填寫圖 9.2 中那兩個空的字段),雖然有點煩人,不過卻保證了安全。
添加了本小節的代碼之后,編輯用戶頁面應該可以正常使用了,你可以運行測試組件再確認一下,測試應該是可以通過的:
$ bundle exec rspec spec/9.2 權限限制
第八章中實現的身份驗證機制有一個很好的作用,可以實現權限限制。身份驗證可以識別用戶是否已經注冊,而權限限制則可以限制用戶可以進行的操作。
雖然?9.1 節中已經基本完成了?edit?和?update?動作,但是卻有一個安全隱患:任何人(甚至是未登錄的用戶)都可以訪問這兩個動作,而且登錄后的用戶可以更新所有其他用戶的資料。本節我們要實現一種安全機制,限制用戶必須先登錄才能更新自己的資料,而不能更新他人的資料。沒有登錄的用戶如果試圖訪問這些受保護的頁面,會轉向登錄頁面,并顯示一個提示信息,構思圖如圖 9.5 所示。
圖 9.5:訪問受保護頁面轉向后的頁面構思圖
9.2.1 必須先登錄
因為對?edit?和?update?動作所做的安全限制是一樣的,所以我們就在同一個 RSpec?describe?塊中進行測試。我們從要求登錄開始,測試代碼要檢測未登錄的用戶試圖訪問這兩個動作時是否轉向了登錄頁面,如代碼 9.11 所示。
代碼 9.11?測試?edit?和?update?動作是否處于被保護狀態
spec/requests/authentication_pages_spec.rb
代碼 9.11 除了使用 Capybara 的?visit?方法之外,還第一次使用了另一種訪問控制器動作的方法:如果需要直接發起某種 HTTP 請求,則直接使用 HTTP 動詞對應的方法即可,例如本例中的?put?發起的就是?PUT?請求:
describe "submitting to the update action" dobefore { put user_path(user) }specify { response.should redirect_to(signin_path) } end上述代碼會向 /users/1 地址發送?PUT?請求,由 Users 控制器的?update?動作處理(參見表格 7.1)。我們必須這么做,因為瀏覽器無法直接訪問?update?動作,必須先提交編輯表單,所以 Capybara 也做不到。訪問編輯資料頁面只能測試?edit?動作是否有權限繼續操作,而不能測試?update?動作的授權情況。所以,如果要測試?update?動作是否有權限進行操作只能直接發送?PUT?請求。(你可能已經猜到了,除了?put?方法之外,Rails 中的測試還支持?get、post?和?delete?方法。)
直接發送某種 HTTP 請求時,我們需要處理更底層的?response?對象。和 Capybara 提供的?page?對象不同,我們可以使用response?測試服務器的響應。本例我們檢測了?update?動作的響應是否轉向了登錄頁面:
specify { response.should redirect_to(signin_path) }我們要使用?before_filter?方法實現權限限制,這個方法會在指定的動作執行之前,先運行指定的方法。為了實現要求用戶先登錄的限制,我們要定義一個名為?signed_in_user?的方法,然后調用?before_filter :signed_in_user,如代碼 9.12 所示。
代碼 9.12?添加?signed_in_user?事前過濾器
app/controllers/users_controller.rb
默認情況下,事前過濾器會應用于控制器中的所有動作,所以在上述代碼中我們傳入了?:only?參數指定只應用在?edit?和update?動作上。
注意,在代碼 9.12 中我們使用了設定?flash[:notice]?的簡便方式,把?redirect_to?方法的第二個參數指定為一個 Hash。這段代碼等同于:
flash[:notice] = "Please sign in." redirect_to signin_path(flash[:error]?也可以使用上述的簡便方式,但?flash[:success]?卻不可以。)
flash[:notice]?加上?flash[:success]?和?flash[:error]?就是我們要介紹的三種 Flash 消息,Bootstrap 為這三種消息都提供了樣式。退出后再嘗試訪問 /users/1/edit,就會看到如圖 9.6 所示的黃色提示框。
圖 9.6:嘗試訪問受保護的頁面后顯示的登錄表單
在嘗試讓代碼 9.11 中檢測權限限制的測試通過的過程中,我們卻破壞了代碼 9.1 中的測試。如下的代碼
describe "edit" dolet(:user) { FactoryGirl.create(:user) }before { visit edit_user_path(user) }...現在會失敗,因為必須先登錄才能正常訪問編輯用戶資料頁面。解決這個問題的辦法是,使用代碼 9.6 中定義的?sign_in?方法登入用戶,如代碼 9.13 所示。
代碼 9.13?為?edit?和?update?測試加入登錄所需的代碼
spec/requests/user_pages_spec.rb
現在所有的測試應該都可以通過了:
$ bundle exec rspec spec/9.2.2 用戶只能編輯自己的資料
當然,要求用戶必須先登錄還是不夠的,用戶必須只能編輯自己的資料。我們的測試可以這么編寫,用其他用戶的身份登錄,然后訪問?edit?和?update?動作,如代碼 9.14 所示。注意,用戶不應該嘗試編輯其他用戶的資料,我們沒有轉向登錄頁面,而是轉到了網站的首頁。
代碼 9.14?測試只有自己才能訪問?edit?和?update?動作
spec/requests/authentication_pages_spec.rb
注意,創建預構件的方法還可以接受第二個參數:
FactoryGirl.create(:user, email: "wrong@example.com")上述代碼會用指定的 Email 替換默認值,然后創建用戶。我們的測試要確保其他的用戶不能訪問原來那個用戶的?edit?和update?動作。
我們在控制器中加入了第二個事前過濾器,調用?correct_user?方法,如代碼 9.15 所示。
代碼 9.15?保護?edit?和?update?動作的?correct_user?事前過濾器
app/controllers/users_controller.rb
上述代碼中的?correct_user?方法使用了?current_user??方法,我們要在 Sessions 幫助方法模塊中定義一下,如代碼 9.16。
代碼 9.16?定義?current_user??方法
app/helpers/sessions_helper.rb
代碼 9.15 同時也更新了?edit?和?update?動作的代碼。之前在代碼 9.2 中,我們是這樣寫的:
def edit@user = User.find(params[:id]) endupdate?代碼類似。既然?correct_user?事前過濾器中已經定義了?@user,這兩個動作中就不再需要再定義?@user?變量了。
在繼續閱讀之前,你應該驗證一下測試是否可以通過:
$ bundle exec rspec spec/9.2.3 更友好的轉向
程序的權限限制基本完成了,但是還有一點小小的不足:不管用戶嘗試訪問的是哪個受保護的頁面,登錄后都會轉向資料頁面。也就是說,如果未登錄的用戶訪問了編輯資料頁面,會要求先登錄,登錄轉到的頁面是 /users/1,而不是/users/1/edit。如果登錄后能轉到用戶之前想訪問的頁面就更好了。
針對這種更友好的轉向,我們可以這樣編寫測試,先訪問編輯用戶資料頁面,轉向登錄頁面后,填寫正確的登錄信息,點擊“Sign in”按鈕,然后顯示的應該是編輯用戶資料頁面,而不是用戶資料頁面。相應的測試如代碼 9.17 所示。
代碼 9.17?測試更友好的轉向
spec/requests/authentication_pages_spec.rb
下面我們來實現這個設想。4要轉向用戶真正想訪問的頁面,我們要在某個地方存儲這個頁面的地址,登錄后再轉向這個頁面。我們要通過兩個方法來實現這個過程,store_location?和?redirect_back_or,都在 Sessions 幫助方法模塊中定義,如代碼 9.18。
代碼 9.18?實現更友好的轉向所需的代碼
app/helpers/sessions_helper.rb
地址的存儲使用了 Rails 提供的?session,session?可以理解成和?8.2.1 節中介紹的?cookies?是類似的東西,會在瀏覽器關閉后自動失效。(在?8.5 節中介紹過,其實?session?的實現方法正是如此。)我們還使用了?request?對象的?fullpath?方法獲取了所請求頁面的完整地址。在?store_location?方法中,把完整的請求地址存儲在?session[:return_to]?中。
要使用?store_location,我們要把它加入?signed_in_user?事前過濾器中,如代碼 9.19 所示。
代碼 9.19?把?store_location?加入?signed_in_user?事前過濾器
app/controllers/users_controller.rb
實現轉向操作,要在 Sessions 控制器的?create?動作中加入?redirect_back_or?方法,用戶登錄后轉到適當的頁面,如代碼 9.20 所示。如果存儲了之前請求的地址,redirect_back_or?方法就會轉向這個地址,否則會轉向參數中指定的地址。
代碼 9.20?加入友好轉向后的?create?動作
app/controllers/sessions_controller.rb
redirect_back_or?方法在下面這行代碼中使用了“或”操作符?||:
session[:return_to] || default如果?session[:return_to]?的值不是?nil,上面這行代碼就會返回?session[:return_to]?的值,否則會返回?default。注意,在代碼 9.18 中,成功轉向后就會刪除存儲在 session 中的轉向地址。如果不刪除的話,在關閉瀏覽器之前,每次登錄后都會轉到存儲的地址上。(對這一過程的測試留作練習,參見?9.6 節。)
加入上述代碼之后,代碼 9.17 中對友好轉向的集成測試應該可以通過了。至此,我們也就完成了基本的用戶身份驗證和頁面保護機制。和之前一樣,在繼續閱讀之前,最好確認一下所有的測試是否都可以通過:
$ bundle exec rspec spec/9.3 列出所有用戶
本節我們要添加計劃中的倒數第二個用戶動作,index。index?動作不會顯示某一個用戶,而是顯示所有的用戶。在這個過程中,我們要學習如何在數據庫中生成示例用戶數據,以及如何分頁顯示用戶列表,顯示任意數量的用戶。用戶列表、分頁鏈接和“所有用戶(Users)”導航鏈接的構思圖如圖 9.7 所示。5在?9.4 節?中,我們還會在用戶列表中添加刪除鏈接,這樣就可以刪除有問題的用戶了。
圖 9.7:用戶列表頁面的構思圖,包含了分頁鏈接和“Users”導航鏈接
9.3.1 用戶列表
單個用戶的資料頁面是對外開放的,不過用戶列表頁面只有注冊用戶才能訪問。我們先來編寫測試。在測試中我們要檢測index?動作是被保護的,如果訪問?users_path?會轉向登錄頁面。和其他的權限限制測試一樣,我們也會把這個測試放在身份驗證的集成測試中,如代碼 9.21 所示。
代碼 9.21?測試?index?動作是否是被保護的
spec/requests/authentication_pages_spec.rb
若要這個測試通過,我們要把?index?動作加入?signed_in_user?事前過濾器,如代碼 9.22 所示。
代碼 9.22?訪問?index?動作必須先登錄
app/controllers/users_controller.rb
接下來,我們要測試用戶登錄后,用戶列表頁面要有特定的標題和標頭,還要列出網站中所有的用戶。為此,我們要創建三個用戶預構件,以第一個用戶的身份登錄,然后檢測用戶列表頁面中是否有一個列表,各用戶的名字都包含在一個單獨的?li標簽中。注意,我們要為每個用戶分配不同的名字,這樣列表中的用戶才是不一樣的,如代碼 9.23 所示。
代碼 9.23?用戶列表頁面的測試
spec/requests/user_pages_spec.rb
你可能還記得,我們在演示程序的相關代碼中介紹過(參見代碼 2.4),在程序中我們可以使用?User.all?從數據庫中取回所有的用戶,賦值給實例變量?@users?在視圖中使用,如代碼 9.24 所示。(你可能會覺得一次列出所有的用戶不太好,你是對的,我們會在?9.3.3 節中改進。)
代碼 9.24?Users 控制器的?index?動作
app/controllers/users_controller.rb
要顯示用戶列表頁面,我們要創建一個視圖,遍歷所有的用戶,把單個用戶包含在?li?標簽中。我們要使用?each?方法遍歷所有用戶,顯示用戶的 Gravatar 頭像和名字,然后把所有的用戶包含在無序列表?ul?標簽中,如代碼 9.25 所示。在代碼 9.25 中,我們用到了?7.6 節練習中代碼 7.29 的成果,允許向 Gravatar 幫助方法傳入第二個參數,指定頭像的大小。如果你之前沒有做這個練習題,在繼續閱讀之前請參照代碼 7.29 更新 Users 控制器的幫助方法文件。
代碼 9.25?用戶列表頁面的視圖
app/views/users/index.html.erb
我們再添加一些 CSS(更確切的說是 SCSS)美化一下,如代碼 9.26。
代碼 9.26?用戶列表頁面的 CSS
app/assets/stylesheets/custom.css.scss
最后,我們還要在頭部的導航中加入到用戶列表頁面的鏈接,鏈接的地址為?users_path,這是表格 7.1中還沒介紹的最后一個具名路由了。相應的測試(代碼 9.27)和程序所需的代碼(代碼 9.28)都很簡單。
代碼 9.27?檢測“Users”鏈接的測試
spec/requests/authentication_pages_spec.rb
代碼 9.28?添加“Users”鏈接
app/views/layouts/_header.html.erb
至此,用戶列表頁面的功能就實現了,所有的測試也都可以通過了:
$ bundle exec rspec spec/不過,如圖 9.8 所示,頁面中只顯示了一個用戶,有點孤單單。下面,讓我們來改變一下這種悲慘狀況。
圖 9.8:用戶列表頁面,只有一個用戶
9.3.2 示例用戶
在本小節中,我們要為應用程序添加更多的用戶。如果要讓用戶列表看上去像個列表,我們可以在瀏覽器中訪問注冊頁面,然后一個一個地注冊用戶,不過還有更好的方法,讓 Ruby 和 Rake 為我們創建用戶。
首先,我們要在?Gemfile?中加入?faker(如代碼 9.29 所示),使用這個 gem,我們可以使用半真實的名字和 Email 地址創建示例用戶。
代碼 9.29?把?faker?加入?Gemfile
source 'https://rubygems.org'gem 'rails', '3.2.13' gem 'bootstrap-sass', '2.0.0' gem 'bcrypt-ruby', '3.0.1' gem 'faker', '1.0.1' . . .然后和之前一樣,運行下面的命令安裝:
$ bundle install接下來我們要添加一個 Rake 任務創建示例用戶。這個 Rake 任務保存在?lib/tasks?文件夾中,而且在?:db?命名空間中定義,如代碼 9.30 所示。(代碼中涉及到一些高級知識,現在不必深入了解。)
代碼 9.30?在數據庫中生成示例用戶的 Rake 任務
lib/tasks/sample_data.rake
上述代碼定義了一個名為?db:populate?的 Rake 任務,先創建一個用戶替代之前存在的那個用戶,然后還創建了 99 個用戶。下面這行代碼
task populate: :environment do確保這個 Rake 任務可以獲取 Rails 環境的信息,包括 User 模型,所以才能使用?User.create!?方法。create!?方法和create?方法的作用一樣,只不過如果提供的信息不合法不會返回?false?而是會拋出異常(參見?6.1.4 節),這樣如果出錯的話就很容易找到錯誤發生的地方。
這個任務是定義在?:db?命名空間中的,所以我們要按照如下的方式來執行:
$ bundle exec rake db:reset $ bundle exec rake db:populate $ bundle exec rake db:test:prepare執行這三個任務之后,我們的應用程序就有 100 個用戶了,如圖 9.9 所示。(我犧牲了一點個人時間為前幾個用戶上傳了頭像,這樣就不都會顯示默認的 Gravatar 頭像了。)
圖 9.9:顯示了 100 個用戶的用戶列表頁面(/users)
9.3.3 分頁
現在,當初的用戶不再孤單單了,但是又出現了新的問題:用戶太多,全在一個頁面中顯示。現在的用戶數量是 100 個,算是少的了,在真實的網站中,這個數量可能是以千計的。為了避免在一頁中顯示過多的用戶,我們可以使用分頁功能,一頁只顯示 30 個用戶。
在 Rails 中有很多實現分頁的方法,我們要使用其中一個最簡單也最完善的,叫做 willpaginate。我們要使用 `willpaginate和bootstrap-willpaginate這兩個 gem,bootstrap-willpaginate的作用是設置 will_paginate 使用 Bootstrap 中的分頁樣式。修改后的Gemfile` 如代碼 9.31 所示。
代碼 9.31?在?Gemfile?中加入 will_paginate
source 'https://rubygems.org'gem 'rails', '3.2.13' gem 'bootstrap-sass', '2.0.0' gem 'bcrypt-ruby', '3.0.1' gem 'faker', '1.0.1' gem 'will_paginate', '3.0.3' gem 'bootstrap-will_paginate', '0.0.6' . . .然后執行下面的命令安裝:
$ bundle install安裝后你還要重啟 Web 服務器,確保成功加載這兩個新 gem。
因為?will_paginate?這個 gem 使用的范圍很廣,所以我們不必做大量的測試,只需簡單的測試一下就可以了。首先我們要檢測頁面中是否包含一個 CSS class 為?pagination?的?div?元素,這個元素就是由 will_paginate 生成的。然后,我們要檢測分頁的第一頁中是否顯示有正確的用戶列表。在測試中我們要用到?paginate?方法,稍后會做介紹。
和之前一樣,我們要使用 Factory Girl 生成用戶,但是我們立馬就會遇到一個問題:因為用戶的 Email 地址必須是唯一的,那么我們就要手動生成 30 個用戶,這可是一件很費事的活兒。而且,在測試用戶列表時,用戶的名字最好也不一樣。幸好 Factory Girl 料事如神,提供了?sequence?方法來解決這種問題。在代碼 7.8 中,我們是直接輸入名字和 Email 地址來創建預構件的:
FactoryGirl.define dofactory :user doname "Michael Hartl"email "michael@example.com"password "foobar"password_confirmation "foobar"end end現在我們要使用?sequence?方法自動創建一系列的名字和 Email 地址:
factory :user dosequence(:name) { |n| "Person #{n}" }sequence(:email) { |n| "person_#{n}@example.com"}...sequence?方法可以接受一個 Symbol 類型的參數,對應到屬性上(例如?:name),其后還可以跟著塊,有一個塊參數,我們將其命名為?n。FactoryGirl.create(:user)?方法執行成功后,塊參數會自動增加 1。因此,創建的第一個用戶名字為“Person 1”,Email 地址為“person1@example.com”;第二個用戶的名字為?Person 2,Email 地址為“person2@example.com”;依此類推。完整的代碼如代碼 9.32 所示。
代碼 9.32?定義 Factory Girl 序列
spec/factories.rb
創建了預構件序列后,在測試中就可以生成 30 個用戶了,這 30 個用戶就可以產生分頁了:
before(:all) { 30.times { FactoryGirl.create(:user) } } after(:all) { User.delete_all }注意,上述代碼使用?before(:all)?確保在塊中所有測試執行之前,一次性創建 30 個示例用戶。這是對速度做的優化,因為在某些系統中每個測試都創建 30 個用戶會很慢。對應的,我們調用?after(:all)?方法,在測試結束后一次性刪除所有的用戶。
代碼 9.33 檢測了頁面中是否包含正確的?div?元素,以及是否顯示了正確的用戶。注意,我們把代碼 9.23 中的?User.all?換成了?User.paginate(page: 1),這樣我們才能從數據庫中取回第一頁中要顯示的用戶。還要注意一下,代碼 9.33 中使用的before(:each)?方法是和?before(:all)?方法相反的操作。
代碼 9.33?測試分頁
spec/requests/user_pages_spec.rb
要實現分頁,我們要在用戶列表頁面的視圖中加入一些代碼,告訴 Rails 要分頁顯示用戶,而且要把?index?動作中的User.all?換成知道如何分頁的方法。我們先在視圖中加入特殊的?will_paginate?方法,如代碼 9.34 所示。稍后我們會看到為什么要在用戶列表的前后都加入分頁代碼。
代碼 9.34?在用戶列表視圖中加入分頁
app/views/users/index.html.erb
will_paginate?方法有點小神奇,在 Users 控制器的視圖中,它會自動尋找名為?@users?的對象,然后顯示一個分頁導航鏈接。代碼 9.34 所示的視圖現在還不能正確顯示分頁,因為現在?@users?的值是通過?User.all?方法獲取的,是個數組;而will_paginate?方法需要的是?ActiveRecord::Relation?類對象。will_paginate 提供的?paginate?方法正好可以返回ActiveRecord::Relation?類對象:
$ rails console >> User.all.class => Array >> User.paginate(page: 1).class => ActiveRecord::Relationpaginate?方法可以接受一個 Hash 類型的參數,鍵?:page?的值指定第幾頁。User.paginate?方法根據?:page?的值,一次取回一系列的用戶(默認為 30 個)。所以,第一頁顯示的是第 1-30 個用戶,第二頁顯示的是第 31-60 個,等。如果指定的頁數不存在,paginate?會顯示第一頁。
我們可以把?index?動作中的?all?方法換成?paginate,這樣頁面中就可以顯示分頁導航了,如代碼 9.35 所示。paginate?方法所需的?:page?參數值由?params[:page]?指定,這個?params?元素是由?will_pagenate?自動生成的。
代碼 9.35?在?index?動作中按分頁取回用戶
app/controllers/users_controller.rb
現在,用戶列表頁面應該可以顯示分頁了,如圖 9.10 所示。(在某些系統中,可能需要重啟 Rails 服務器。)因為我們在用戶列表前后都加入了?will_paginate?方法,所以這兩個地方都會顯示分頁鏈接。
圖 9.10:顯示了分頁鏈接的用戶列表頁面(/users)
如果點擊鏈接“2”,或者“Next”,就會顯示第二頁,如圖 9.11 所示。
圖 9.11:用戶列表的第二頁(/users?page=2)
你還應該驗證一下測試是否可以通過:
$ bundle exec rspec spec/9.3.4 視圖重構
用戶列表頁面現在已經可以顯示分頁了,但是有個改進點我不得不介紹一下。Rails 提供了一些很巧妙的方法可以精簡視圖的結構,本小節我們就要利用這些方法重構一下用戶列表頁面。因為我們已經做了很好的測試,所以就可以放手去重構,不用擔心會破壞網站的功能。
重構的第一步,要把代碼 9.34 中的?li?換成對?render?方法的調用,如代碼 9.36 所示。
代碼 9.36?重構用戶列表視圖的第一步
app/views/users/index.html.erb
在上述代碼中,render?的參數不再是指定局部視圖的字符串,而是代表?User?類的?user?變量。6Rails 會自動尋找一個名為_user.html.erb?的局部視圖,我們要手動創建這個視圖,然后寫入代碼 9.37 中的內容。
代碼 9.37?顯示單一用戶的局部視圖
app/views/users/_user.html.erb
這個改進很不錯,不過我們還可以做的更好。我們可以直接把?@users?變量傳遞給?render?方法,如代碼 9.38 所示。
代碼 9.38?完全重構后的用戶列表視圖
app/views/users/index.html.erb
Rails 會把?@users?當作一系列的?User?對象,遍歷這些對象,然后使用?_user.html.erb?渲染每個對象。所以我們就得到了代碼 9.38 這樣簡潔的代碼。每次重構后,你都應該驗證一下測試組件是否還是可以通過的:
$ bundle exec rspec spec/9.4 刪除用戶
至此,用戶索引也完成了。符合 REST 架構的Users 資源就只剩下最后一個?destroy?動作了。本節,我們先添加刪除用戶的鏈接(構思圖如圖 9.12 所示),然后再編寫適當的?destroy?動作代碼完成刪除操作。不過,首先我們要先創建管理員級別的用戶,并授權這些用戶進行刪除操作。
圖 9.12:顯示有刪除鏈接的用戶列表頁面構思圖
9.4.1 管理員
我們要通過 User 模型中一個名為?admin?的屬性來判斷用戶是否具有管理員權限。admin?屬性的類型為布爾值,Active Record 會自動生成一個?admin??方法,返回布爾值,判斷用戶是否為管理員。針對?admin?屬性的測試如代碼 9.39 所示。
代碼 9.39?測試?admin?屬性
spec/models/user_spec.rb
在上述代碼中我們使用?toggle!?方法把?admin?屬性的值從?false?轉變成?true。it { should be_admin }?這行代碼說明用戶對象應該可以響應?admin??方法(這是 RSpec 對布爾值屬性的一個約定)。
和之前一樣,我們要使用遷移添加?admin?屬性,在命令行中指定其類型為?boolean:
$ rails generate migration add_admin_to_users admin:boolean這個命令生成的遷移文件(如代碼 9.40 所示)會在 users 表中添加?admin?這一列,得到的數據模型如圖 9.13 所示。
圖 9.13:添加了?admin?屬性后的 User 模型
代碼 9.40?為 User 模型添加?admin?屬性所用的遷移文件
db/migrate/[timestamp]_add_admin_to_users.rb
注意,在代碼 9.40 中,我們為?add_column?方法指定了?default: false?參數,添加這個參數后用戶默認情況下就不是管理員。(如果沒有指定?default: false,admin?的默認值是?nil,也是“假值”,所以嚴格來說,這個參數不是必須的。不過,指定這個參數,可以更明確地向 Rails 以及代碼的閱讀者表明這段代碼的意圖。)
然后,我們要在“開發數據庫”中執行遷移操作,還要準備好“測試數據庫”:
$ bundle exec rake db:migrate $ bundle exec rake db:test:prepare和預想的一樣,Rails 可以自動識別?admin?屬性的類型為布爾值,而且自動生成了?admin??方法:
$ rails console --sandbox >> user = User.first >> user.admin? => false >> user.toggle!(:admin) => true >> user.admin? => true執行遷移操作后,針對?admin?屬性的測試應該可以通過了:
$ bundle exec rspec spec/models/user_spec.rb最后,我們要修改一下生成示例用戶的代碼,把第一個用戶設為管理員,如代碼 9.41 所示。
代碼 9.41?生成示例用戶的代碼,把第一個用戶設為管理員
lib/tasks/sample_data.rake
之后還要還原數據庫,并且重新生成示例用戶:
$ bundle exec rake db:reset $ bundle exec rake db:populate $ bundle exec rake db:test:prepareattr_accessible?再探
你可能注意到了,在代碼 9.41 中,我們使用?toggle!(:admin)?把用戶設為管理員,為什么沒有直接在?User.create!?的參數中指定?admin: true?呢?原因是,直接指定?admin: true?不起作用,Rails 就是這樣設計的,只有通過?attr_accessible?指定的屬性才能通過 mass assignment 賦值,而?admin?并不是可訪問的。代碼 9.42 顯示的是當前可訪問的屬性列表,注意其中并沒有?:admin。
代碼 9.42?User 模型中通過?attr_accessible?指定的可訪問的屬性,其中沒有?:admin?屬性
app/models/user.rb
明確指定可訪問的屬性對網站的安全是很重要的,如果你沒有指定,或者傻傻的把?:admin?也加進去了,那么心懷不軌的用戶就可以發送下面這個?PUT?請求:7
put /users/17?admin=1這個請求會把 id 為 17 的用戶設為管理員,這可是一個很嚴重的安全隱患。鑒于此,最佳的方法是在每個數據模型中都指定可訪問的屬性列表。其實,最好再測試一下各屬性是否是可訪問的,對?:admin?屬性的可訪問性測試留作練習,參見?9.6 節。
9.4.2?destroy?動作
編寫完整的 Users 資源還要再添加刪除鏈接和?destroy?動作。我們先在用戶列表頁面每個用戶后面都加入一個刪除鏈接,而且限制只有管理員才能看到這些鏈接。
編寫針對刪除功能的測試,最好能有個創建管理員的工廠方法,為此,我們可以在預構件中加入一個名為?:admin?的塊,如代碼 9.43 所示。
代碼 9.43?添加一個創建管理員的工廠方法
spec/factories.rb
添加了以上代碼之后,我們就可以在測試中調用?FactoryGirl.create(:admin)?創建管理員用戶了。
基于安全考慮,普通用戶是看不到刪除用戶鏈接的,所以:
it { should_not have_link('delete') }只有管理員才能看到刪除用戶鏈接,如果管理員點擊了刪除用戶鏈接,該用戶會被刪除,用戶的數量就會減少 1 個:
it { should have_link('delete', href: user_path(User.first)) } it "should be able to delete another user" doexpect { click_link('delete') }.to change(User, :count).by(-1) end it { should_not have_link('delete', href: user_path(admin)) }注意,我們還添加了一個測試,確保管理員不會看到刪除自己的鏈接。針對刪除用戶的完整測試如代碼 9.44 所示。
代碼 9.44?測試刪除用戶功能
spec/requests/user_pages_spec.rb
然后在視圖中加入代碼 9.45。注意鏈接中的?method: delete?參數,它指明點擊鏈接后發送的是?DELETE?請求。我們還把各鏈接放在了?if?語句中,這樣就只有管理員才能看到刪除用戶鏈接。管理員看到的頁面如圖 9.14 所示。
代碼 9.45?刪除用戶的鏈接(只有管理員才能看到)
app/views/users/_user.html.erb
圖 9.14:顯示有刪除用戶鏈接的用戶列表頁面(/users)
瀏覽器不能發送?DELETE?請求,Rails 通過 JavaScript 進行模擬的。也就是說,如果用戶禁用了 JavaScript,那么刪除用戶的鏈接就不可用了。如果必須要支持沒有啟用 JavaScript 的瀏覽器,你可以使用一個發送?POST?請求的表單來模擬?DELETE?請求,這樣即使瀏覽器的 JavaScript 被禁用了,刪除用戶的鏈接還是可用的,更多細節請觀看 RailsCasts 第 77 集《Destroy Without JavaScript》。
若要刪除用戶的鏈接起作用,我們要定義?destroy?動作(參見表格 7.1)。在?destroy?動作中,先找到要刪除的用戶,使用 Active Record 提供的?destroy?方法刪除這個用戶,然后再轉向用戶列表頁面,如代碼 9.46 所示。
代碼 9.46?加入?destroy?動作
app/controllers/users_controller.rb
注意上述?destroy?動作中,把?find?方法和?destroy?方法鏈在一起使用了:
User.find(params[:id]).destroy理論上,只有管理員才能看到刪除用戶的鏈接,所以只有管理員才能刪除用戶。但實際上,還是存在一個嚴重的安全隱患:只要攻擊者有足夠的經驗,就可以在命令行中發送?DELETE?請求,刪除網站中的用戶。為了保證網站的安全,我們還要限制對?destroy?動作的訪問,因此我們在測試中不僅要確保只有管理員才能刪除用戶,還要保證其他用戶不能執行刪除操作,如代碼 9.47 所示。注意,和代碼 9.11 中的?put?方法類似,在這段代碼中我們使用?delete?方法向指定的地址(user_path,參見表格 7.1)發送了一個?DELETE?請求。
代碼 9.47?測試訪問受限的?destroy?動作
spec/requests/authentication_pages_spec.rb
理論上來說,網站中還是有一個安全漏洞,管理員可以發送?DELETE?請求刪除自己。有些人可能會想,這樣的管理員是自作自受。不過作為開發人員,我們最好還是要避免這種情況的發生,具體的實現留作練習,參見?9.6 節。
你可能已經知道了,我們要使用一個事前過濾器限制對?destroy?動作的訪問,如代碼 9.48 所示。
代碼 9.48?限制只有管理員才能訪問?destroy?動作的事前過濾器
app/controllers/users_controller.rb
至此,所有的測試應該都可以通過了,而且 Users 相關的資源,包括控制器、模型和視圖,都已經實現了。
$ bundle exec rspec spec/9.5 小結
我們用了好幾章來介紹如何實現 Users 資源,在?5.4 節用戶還不能注冊,而現在不僅可以注冊,還可以登錄、退出、查看個人資料、修改設置,還能瀏覽網站中所有的用戶列表,某些用戶甚至可以刪除其他的用戶。
本書剩下的內容會以這個 Users 資源為基礎(以及相關的權限授權系統),在第十章中為示例程序加入類似 Twitter 的微博功能,在第十一章中實現關注用戶的狀態列表。最后這兩章會介紹幾個 Rails 中最為強大的功能,其中就包括通過?has_many和?has_many through?實現的數據模型關聯。
在繼續閱讀之前,先把本章所做的改動合并到主分支:
$ git add . $ git commit -m "Finish user edit, update, index, and destroy actions" $ git checkout master $ git merge updating-users你還可以將程序部署到“生產環境”,再生成示例用戶(在此之前要使用?pg:reset?命令還原“生產數據庫”):
$ git push heroku $ heroku pg:reset DATABASE $ heroku run rake db:migrate $ heroku run rake db:populate(如果你忘了 Heroku 程序的名字,可以直接運行?heroku pg:reset DATABASE,Heroku 會告訴你程序的名字。)
還有一點需要注意,本章我們加入了程序所需的最后一個 gem,最終的?Gemfile?如代碼 9.49 所示。
代碼 9.49?示例程序所需?Gemfile?的最終版本
source 'https://rubygems.org'gem 'rails', '3.2.13' gem 'bootstrap-sass', '2.0.0' gem 'bcrypt-ruby', '3.0.1' gem 'faker', '1.0.1' gem 'will_paginate', '3.0.3' gem 'bootstrap-will_paginate', '0.0.6'group :development dogem 'sqlite3', '1.3.5'gem 'annotate', '?> 2.4.1.beta' end# Gems used only for assets and not required # in production environments by default. group :assets dogem 'sass-rails', '3.2.4'gem 'coffee-rails', '3.2.2'gem 'uglifier', '1.2.3' endgem 'jquery-rails', '2.0.0'group :test, :development dogem 'rspec-rails', '2.10.0'gem 'guard-rspec', '0.5.5'gem 'guard-spork', '0.3.2'gem 'spork', '0.9.0' endgroup :test dogem 'capybara', '1.1.2'gem 'factory_girl_rails', '1.4.0'gem 'cucumber-rails', '1.2.1', require: falsegem 'database_cleaner', '0.7.0' endgroup :production dogem 'pg', '0.12.2' end9.6 練習
代碼 9.50?注冊和編輯表單字段的局部視圖
app/views/users/_fields.html.erb
代碼 9.51?使用局部視圖后的注冊頁面視圖
app/views/users/new.html.erb
代碼 9.52?測試友好的轉向后,只能轉向到默認的頁面
spec/requests/authentication_pages_spec.rb
總結
以上是生活随笔為你收集整理的Python计算机视觉:第九章 图像分割的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python计算机视觉:第八章 图像类容
- 下一篇: Python计算机视觉:第十章 Open