-
Notifications
You must be signed in to change notification settings - Fork 2
/
chapter9.html
1871 lines (1569 loc) · 180 KB
/
chapter9.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="zh_CN">
<head>
<meta charset="utf-8" />
<title>第 9 章 更新、显示和删除用户</title>
<meta name="author" content="Andor Chen" />
<link rel="stylesheet" href="assets/styles/style.css" />
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript" src="assets/js/global.js"></script>
</head>
<body>
<div class="wrapper">
<div class="header">
<h1 class="logo"><a class="ir" href="http://railstutorial-china.org/rails4">Ruby on Rails 教程</a></h1>
<p class="subtitle">Ruby on Rails Tutorial 原书第 2 版(涵盖 Rails 4)</p>
</div>
<div class="content">
<div class="item chapter">
<h1 id="chapter-9"><span>第 9 章</span> 更新、显示和删除用户</h1>
<ol class="toc"> <li class="level-2">
<a href="#section-9-1">9.1 更新用户</a>
</li>
<li class="level-3">
<a href="#section-9-1-1">9.1.1 编辑表单</a>
</li>
<li class="level-3">
<a href="#section-9-1-2">9.1.2 编辑失败</a>
</li>
<li class="level-3">
<a href="#section-9-1-3">9.1.3 编辑成功</a>
</li>
<li class="level-2">
<a href="#section-9-2">9.2 权限限制</a>
</li>
<li class="level-3">
<a href="#section-9-2-1">9.2.1 必须先登录</a>
</li>
<li class="level-3">
<a href="#section-9-2-2">9.2.2 用户只能编辑自己的资料</a>
</li>
<li class="level-3">
<a href="#section-9-2-3">9.2.3 更友好的转向</a>
</li>
<li class="level-2">
<a href="#section-9-3">9.3 列出所有用户</a>
</li>
<li class="level-3">
<a href="#section-9-3-1">9.3.1 用户列表</a>
</li>
<li class="level-3">
<a href="#section-9-3-2">9.3.2 示例用户</a>
</li>
<li class="level-3">
<a href="#section-9-3-3">9.3.3 分页</a>
</li>
<li class="level-3">
<a href="#section-9-3-4">9.3.4 视图重构</a>
</li>
<li class="level-2">
<a href="#section-9-4">9.4 删除用户</a>
</li>
<li class="level-3">
<a href="#section-9-4-1">9.4.1 管理员</a>
</li>
<li class="level-3">
<a href="#section-9-4-2">9.4.2 destroy 动作</a>
</li>
<li class="level-2">
<a href="#section-9-5">9.5 小结</a>
</li>
<li class="level-2">
<a href="#section-9-6">9.6 练习</a>
</li>
</ol>
<div class="main">
<p>本章我们要完成<a href="chapter7.html#table-7-1">表格 7.1</a>所示的Users 资源,添加 <code>edit</code>、<code>update</code>、<code>index</code> 和 <code>destroy</code> 动作。首先我们要实现更新用户个人资料的功能,实现这样的功能自然要依靠安全验证系统(基于<a href="chapter8.html">第 8 章</a>中实现的权限限制))。然后要创建一个页面列出所有的用户(也需要权限限制),期间会介绍示例数据和分页功能。最后,我们还要实现删除用户的功能,从数据库中删除用户记录。我们不会为所有用户都提供这种强大的权限,而是会创建管理员,授权他们来删除用户。</p>
<p>在开始之前,我们要新建 <code>updating-users</code> 分支:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>git checkout -b updating-users
</pre></div>
</div>
<h2 id='section-9-1'><span>9.1</span> 更新用户</h2>
<p>编辑用户信息的方法和创建新用户差不多(参见<a href="chapter7.html">第 7 章</a>),创建新用户的页面是在 <code>new</code> 动作中处理的,而编辑用户的页面则是在 <code>edit</code> 动作中;创建用户的过程是在 <code>create</code> 动作中处理了 <code>POST</code> 请求,而编辑用户要在 <code>update</code> 动作中处理 <code>PATCH</code> 请求(HTTP 请求参见<a href="chapter3.html#aside-3-3">旁注 3.3</a>)。二者之间最大的区别是,任何人都可以注册,但只有当前用户才能更新他自己的信息。所以我们就要限制访问,只有授权的用户才能编辑更新资料,我们可以利用<a href="chapter8.html">第 8 章</a>实现的身份验证机制,使用”事前过滤器(before filter)“实现访问限制。</p>
<h3 id='section-9-1-1'><span>9.1.1</span> 编辑表单</h3>
<p>我们先来创建编辑表单,其构思图如图 9.1 所示。<sup class="footnote" id="fnref-9-1"><a href="#fn-9-1" rel="footnote">1</a></sup>和之前一样,我们要先编写测试。注意构思图中修改 Gravatar 头像的链接,如果你浏览过 Gravatar 的网站,可能就知道上传和编辑头像的地址是 http://gravatar.com/emails,我们就来测试编辑页面中有没有一个链接指向了这个地址。<sup class="footnote" id="fnref-9-2"><a href="#fn-9-2" rel="footnote">2</a></sup></p>
<p>对编辑用户表单的测试和第七章练习中的代码 7.31 类似,同样也测试了提交不合法数据后是否会显示错误提示信息,如代码 9.1 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-1"><p class="caption"><span>代码 9.1:</span>用户编辑页面的测试</p><p class="file"><code>spec/requests/user_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"User pages"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"edit"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"page"</span> <span class="k">do</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="s2">"Update your profile"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="s2">"Edit user"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'change'</span><span class="p">,</span> <span class="ss">href: </span><span class="s1">'http://gravatar.com/emails'</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"with invalid information"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">click_button</span> <span class="s2">"Save changes"</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'error'</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="figure" id="figure-9-1">
<img src="figures/edit_user_mockup_bootstrap.png" alt="edit user mockup bootstrap" />
<p class="caption"><span>图 9.1:</span>编辑用户页面的构思图</p>
</div>
<p>程序所需的代码要放在 <code>edit</code> 动作中,我们在<a href="chapter7.html#table-7-1">表格 7.1</a>中列出了,用户编辑页面的地址是 /users/1/edit(假设用户的 id 是 1)。我们介绍过用户的 id 是保存在 <code>params[:id]</code> 中的,所以我们可以按照代码 9.2 所示的方法查找用户。</p>
<div class="codeblock has-caption" id="codeblock-9-2"><p class="caption"><span>代码 9.2:</span>Users 控制器的 <code>edit</code> 方法</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">edit</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>要让测试通过,我们就要编写编辑用户页面的视图,如代码 9.3 所示。仔细观察一下视图代码,它和代码 7.17 中创建新用户页面的视图代码很相似,这就暗示我们要进行重构,把重复的代码移入局部视图。重构会留作练习,详情参见 <a href="chapter9.html#section-9-6">9.6 节</a>。</p>
<div class="codeblock has-caption" id="codeblock-9-3"><p class="caption"><span>代码 9.3:</span>编辑用户页面的视图</p><p class="file"><code>app/views/users/edit.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s2">"Edit user"</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><h1></span>Update your profile<span class="nt"></h1></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"row"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"span6 offset3"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/error_messages'</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:email</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:email</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:password</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:password_confirmation</span><span class="p">,</span> <span class="s2">"Confirm Password"</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password_confirmation</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Save changes"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"btn btn-large btn-primary"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"http://gravatar.com/emails"</span><span class="nt">></span>change<span class="nt"></a></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
</pre></div>
</div>
<p>在这段代码中我们再次使用了 <a href="chapter7.html#section-7-3-3">7.3.3 节</a>中创建的 <code>error_messages</code> 局部视图。</p>
<p>添加了视图代码,再加上代码 9.2 中定义的 <code>@user</code> 变量,代码 9.1 中的“编辑页面”测试应该就可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/requests/user_pages_spec.rb -e <span class="s2">"edit page"</span>
</pre></div>
</div>
<p>编辑用户页面如图 9.2 所示,我们看到 Rails 会自动读取 <code>@user</code> 变量,预先填好了名字和 Email 地址字段。</p>
<div class="figure" id="figure-9-2">
<img src="figures/edit_page_bootstrap.png" alt="edit page bootstrap" />
<p class="caption"><span>图 9.2:</span>编辑用户页面,名字和 Email 地址字段已经自动填好了</p>
</div>
<p>查看一下编辑用户页面的源码,我们可以发现的确生成了一个 <code>form</code> 元素,参见代码 9.4。</p>
<div class="codeblock has-caption" id="codeblock-9-4"><p class="caption"><span>代码 9.4:</span>编辑表单的 HTML</p><div class="highlight type-html"><pre><span class="nt"><form</span> <span class="na">action=</span><span class="s">"/users/1"</span> <span class="na">class=</span><span class="s">"edit_user"</span> <span class="na">id=</span><span class="s">"edit_user_1"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">name=</span><span class="s">"_method"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"patch"</span> <span class="nt">/></span>
.
.
.
<span class="nt"></form></span>
</pre></div>
</div>
<p>留意一下其中的一个隐藏字段:</p>
<div class="codeblock"><div class="highlight type-html"><pre><span class="nt"><input</span> <span class="na">name=</span><span class="s">"_method"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"patch"</span> <span class="nt">/></span>
</pre></div>
</div>
<p>因为浏览器本身并不支持发送 <code>PATCH</code> 请求(<a href="chapter7.html#table-7-1">表格 7.1</a>中列出的 REST 动作要用),所以 Rails 就在 <code>POST</code> 请求中使用这个隐藏字段伪造了一个 <code>PATCH</code> 请求。<sup class="footnote" id="fnref-9-3"><a href="#fn-9-3" rel="footnote">3</a></sup></p>
<p>还有一个细节需要注意一下,代码 9.3 和代码 7.17 都使用了相同的 <code>form_for(@user)</code> 来构建表单,那么 Rails 是怎么知道创建新用户要发送 <code>POST</code> 请求,而编辑用户时要发送 <code>PATCH</code> 请求的呢?这个问题的答案是,通过 Active Record 提供的 <code>new_record?</code> 方法可以检测用户是新创建的还是已经存在于数据库中的:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>rails console
<span class="gp">>> </span>User.new.new_record?
<span class="gp">=> </span><span class="nb">true</span>
<span class="gp">>> </span>User.first.new_record?
<span class="gp">=> </span><span class="nb">false</span>
</pre></div>
</div>
<p>所以在使用 <code>form_for(@user)</code> 构建表单时,如果 <code>@user.new_record?</code> 返回 <code>true</code> 则发送 <code>POST</code> 请求,否则就发送 <code>PATCH</code> 请求。</p>
<p>最后,我们还要在导航中添加一个指向编辑用户页面的链接(“设置(Settings)”)。因为只有登录之后才会显示这个页面,所以对“设置”链接的测试要和其他的身份验证测试放在一起,如代码 9.5 所示。(如果能再测试一下没登录时不会显示“设置”链接就更完美了,这会留作练习,参见 <a href="chapter9.html#section-9-6">9.6 节</a>。)</p>
<div class="codeblock has-caption" id="codeblock-9-5"><p class="caption"><span>代码 9.5:</span>添加检测“设置”链接的测试</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"with valid information"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">sign_in</span> <span class="n">user</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Profile'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Settings'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">edit_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Sign out'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">signout_path</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should_not</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Sign in'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">signin_path</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>为了简化,代码 9.5 中使用 <code>sign_in</code> 帮助方法,这个方法的作用是访问登录页面,提交合法的表单数据,如代码 9.6 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-6"><p class="caption"><span>代码 9.6:</span>用户登录帮助方法</p><p class="file"><code>spec/support/utilities.rb</code></p><div class="highlight type-ruby"><pre><span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">sign_in</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">options</span><span class="o">=</span><span class="p">{})</span>
<span class="k">if</span> <span class="n">options</span><span class="p">[</span><span class="ss">:no_capybara</span><span class="p">]</span>
<span class="c1"># Sign in when not using Capybara.</span>
<span class="n">remember_token</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new_remember_token</span>
<span class="n">cookies</span><span class="p">[</span><span class="ss">:remember_token</span><span class="p">]</span> <span class="o">=</span> <span class="n">remember_token</span>
<span class="n">user</span><span class="p">.</span><span class="nf">update_attribute</span><span class="p">(</span><span class="ss">:remember_token</span><span class="p">,</span> <span class="no">User</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="n">remember_token</span><span class="p">))</span>
<span class="k">else</span>
<span class="n">visit</span> <span class="n">signin_path</span>
<span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">user</span><span class="p">.</span><span class="nf">email</span>
<span class="n">fill_in</span> <span class="s2">"Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">user</span><span class="p">.</span><span class="nf">password</span>
<span class="n">click_button</span> <span class="s2">"Sign in"</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>如上述代码中的注释所说,如果没有使用 Capybara 的话,填写表单的操作是无效的,所以我们提供了 <code>no_capybara: true</code> 选项,跳过默认的登录操作,直接处理 cookie。如果直接使用 HTTP 请求方法就必须要有上面这行代码,具体的用法在代码 9.46 中有介绍。(注意,测试中使用的 <code>cookies</code> 对象和真实的 cookies 对象是有点不一样的,代码 8.19 中使用的 <code>cookies.permanent</code> 方法不能在测试中使用。)你可能已经猜到了,<code>sign_in</code> 在后续的测试中还会用到,而且还可以用来去除重复代码(参见 <a href="chapter9.html#section-9-6">9.6 节</a>)。</p>
<p>在程序中添加“设置”链接很简单,我们就直接使用<a href="chapter7.html#table-7-1">表格 7.1</a> 中列出的 <code>edit_user_path</code> 具名路由,其参数设为代码 8.22 中定义的 <code>current_user</code> 帮助方法:</p>
<div class="codeblock"><div class="highlight type-erb"><pre><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Settings"</span><span class="p">,</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span> <span class="cp">%></span>
</pre></div>
</div>
<p>完整的代码如代码 9.7 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-7"><p class="caption"><span>代码 9.7:</span>添加“设置”链接</p><p class="file"><code>app/views/layouts/_header.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><header</span> <span class="na">class=</span><span class="s">"navbar navbar-fixed-top navbar-inverse"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"navbar-inner"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"sample app"</span><span class="p">,</span> <span class="n">root_path</span><span class="p">,</span> <span class="ss">id: </span><span class="s2">"logo"</span> <span class="cp">%></span>
<span class="nt"><nav></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"nav pull-right"</span><span class="nt">></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Home"</span><span class="p">,</span> <span class="n">root_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Help"</span><span class="p">,</span> <span class="n">help_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="n">signed_in?</span> <span class="cp">%></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Users"</span><span class="p">,</span> <span class="s1">'#'</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">id=</span><span class="s">"fat-menu"</span> <span class="na">class=</span><span class="s">"dropdown"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"dropdown-toggle"</span> <span class="na">data-toggle=</span><span class="s">"dropdown"</span><span class="nt">></span>
Account <span class="nt"><b</span> <span class="na">class=</span><span class="s">"caret"</span><span class="nt">></b></span>
<span class="nt"></a></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"dropdown-menu"</span><span class="nt">></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Profile"</span><span class="p">,</span> <span class="n">current_user</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Settings"</span><span class="p">,</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"divider"</span><span class="nt">></li></span>
<span class="nt"><li></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign out"</span><span class="p">,</span> <span class="n">signout_path</span><span class="p">,</span> <span class="ss">method: </span><span class="s2">"delete"</span> <span class="cp">%></span>
<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign in"</span><span class="p">,</span> <span class="n">signin_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></ul></span>
<span class="nt"></nav></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></header></span>
</pre></div>
</div>
<h3 id='section-9-1-2'><span>9.1.2</span> 编辑失败</h3>
<p>本小节我们要处理编辑失败的情况,让代码 9.1 中对错误提示信息的测试通过。我们要在 Users 控制器的 <code>update</code> 动作中使用 <code>update_attributes</code> 方法,传入提交的 <code>params</code> Hash,更新用户记录,如代码 9.8 所示。如果提交了不合法的数据,更新操作会返回 <code>false</code>,交由 <code>else</code> 分支处理,重新渲染编辑用户页面。我们之前用过类似的处理方式,代码结构和第一个版本的 <code>create</code> 动作类似(参见代码 7.21)。</p>
<div class="codeblock has-caption" id="codeblock-9-8"><p class="caption"><span>代码 9.8:</span>还不完整的 <code>update</code> 动作</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">edit</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">update_attributes</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="c1"># Handle a successful update.</span>
<span class="k">else</span>
<span class="n">render</span> <span class="s1">'edit'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>注意,在调用 <code>update_attributes</code> 方法时指定的 <code>user_params</code> 参数,这种用法是“健壮参数”(strong parameter),可以避免 mass assignment 漏洞(参见 <a href="chapter7.html#section-7-3-2">7.3.2 节</a>)。</p>
<div class="figure" id="figure-9-3">
<img src="figures/edit_with_invalid_information_bootstrap.png" alt="edit with invalid information bootstrap" />
<p class="caption"><span>图 9.3:</span>提交编辑表单后显示的错误提示信息</p>
</div>
<p>提交不合法信息后显示了错误提示信息(如图 9.3),测试就可以通过了,你可以运行测试组件验证一下:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h3 id='section-9-1-3'><span>9.1.3</span> 编辑成功</h3>
<p>现在我们要让编辑表单能够正常使用了。编辑头像的功能已经实现了,因为我们把上传头像的操作交由 Gravatar 处理了,如需更换头像,点击图 9.2 中的“change”链接就可以了,如图 9.4 所示。下面我们来实现编辑其他信息的功能。</p>
<div class="figure" id="figure-9-4">
<img src="figures/gravatar_cropper.png" alt="gravatar cropper" />
<p class="caption"><span>图 9.4:</span>Gravatar 的剪切图片界面,上传了一个帅哥的图片</p>
</div>
<p>对 <code>update</code> 动作的测试和对 <code>create</code> 的测试类似。代码 9.9 介绍了如何使用 Capybara 在表单中填写合法的数据,还介绍了怎么测试提交表单的操作是否正确。测试的代码很多,你可以参考<a href="chapter7.html">第 7 章</a>中的测试,试一下能不能完全理解。</p>
<div class="codeblock has-caption" id="codeblock-9-9"><p class="caption"><span>代码 9.9:</span>测试 Users 控制器的 <code>update</code> 动作</p><p class="file"><code>spec/requests/user_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"User pages"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"edit"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="k">do</span>
<span class="n">sign_in</span> <span class="n">user</span>
<span class="n">visit</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"with valid information"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:new_name</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"New Name"</span> <span class="p">}</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:new_email</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"[email protected]"</span> <span class="p">}</span>
<span class="n">before</span> <span class="k">do</span>
<span class="n">fill_in</span> <span class="s2">"Name"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">new_name</span>
<span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">new_email</span>
<span class="n">fill_in</span> <span class="s2">"Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">user</span><span class="p">.</span><span class="nf">password</span>
<span class="n">fill_in</span> <span class="s2">"Confirm Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">user</span><span class="p">.</span><span class="nf">password</span>
<span class="n">click_button</span> <span class="s2">"Save changes"</span>
<span class="k">end</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">new_name</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_selector</span><span class="p">(</span><span class="s1">'div.alert.alert-success'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Sign out'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">signout_path</span><span class="p">)</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">reload</span><span class="p">.</span><span class="nf">name</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="n">new_name</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">reload</span><span class="p">.</span><span class="nf">email</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="n">new_email</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>上述代码中出现了一个新的方法 <code>reload</code>,出现在检测用户的属性是否已经更新的测试中:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">reload</span><span class="p">.</span><span class="nf">name</span><span class="p">).</span><span class="nf">to</span> <span class="o">==</span> <span class="n">new_name</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">reload</span><span class="p">.</span><span class="nf">email</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="n">new_email</span> <span class="p">}</span>
</pre></div>
</div>
<p>这两行代码使用 <code>user.reload</code> 从测试数据库中重新加载 <code>user</code> 的数据,然后检测用户的名字和 Email 地址是否更新成了新的值。</p>
<p>要让代码 9.9 中的测试通过,我们可以参照最终版本的 <code>create</code> 动作(代码 8.27)来编写 <code>update</code> 动作,如代码 9.10 所示。我们在代码 9.8 的基础上加入了下面这三行。</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">flash</span><span class="p">[</span><span class="ss">:success</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Profile updated"</span>
<span class="n">sign_in</span> <span class="vi">@user</span>
<span class="n">redirect_to</span> <span class="vi">@user</span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-9-10"><p class="caption"><span>代码 9.10:</span>Users 控制器的 <code>update</code> 动作</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">update</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">update_attributes</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="n">flash</span><span class="p">[</span><span class="ss">:success</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Profile updated"</span>
<span class="n">redirect_to</span> <span class="vi">@user</span>
<span class="k">else</span>
<span class="n">render</span> <span class="s1">'edit'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>注意,现在这种实现方式,每次更新数据都要提供密码(填写图 9.2 中那两个空的字段),虽然有点烦人,不过却保证了安全。</p>
<p>添加了本小节的代码之后,编辑用户页面应该可以正常使用了,你可以运行测试组件再确认一下,测试应该是可以通过的:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h2 id='section-9-2'><span>9.2</span> 权限限制</h2>
<p><a href="chapter8.html">第 8 章</a>中实现的身份验证机制有一个很好的作用,可以实现权限限制。身份验证可以识别用户是否已经注册,而权限限制则可以限制用户可以进行的操作。</p>
<p>虽然 <a href="chapter9.html#section-9-1">9.1 节</a>中已经基本完成了 <code>edit</code> 和 <code>update</code> 动作,但是却有一个安全隐患:任何人(甚至是未登录的用户)都可以访问这两个动作,而且登录后的用户可以更新所有其他用户的资料。本节我们要实现一种安全机制,限制用户必须先登录才能更新自己的资料,而不能更新他人的资料。没有登录的用户如果试图访问这些受保护的页面,会转向登录页面,并显示一个提示信息,构思图如图 9.5 所示。</p>
<h3 id='section-9-2-1'><span>9.2.1</span> 必须先登录</h3>
<p>因为对 <code>edit</code> 和 <code>update</code> 动作所做的安全限制是一样的,所以我们就在同一个 RSpec <code>describe</code> 块中进行测试。我们从要求登录开始,测试代码要检测未登录的用户试图访问这两个动作时是否转向了登录页面,如代码 9.11 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-11"><p class="caption"><span>代码 9.11:</span>测试 <code>edit</code> 和 <code>update</code> 动作是否处于被保护状态</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"authorization"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"for non-signed-in users"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"in the Users controller"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"visiting the edit page"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="s1">'Sign in'</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"submitting to the update action"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">patch</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">signin_path</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>代码 9.11 除了使用 Capybara 的 <code>visit</code> 方法之外,还第一次使用了另一种访问控制器动作的方法:如果需要直接发起某种 HTTP 请求,则直接使用 HTTP 动词对应的方法即可,例如本例中的 <code>patch</code> 发起的就是 <code>PATCH</code> 请求:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">describe</span> <span class="s2">"submitting to the update action"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">patch</span> <span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">signin_path</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
</pre></div>
</div>
<p>上述代码会向 /users/1 地址发送 <code>PATCH</code> 请求,由 Users 控制器的 <code>update</code> 动作处理(参见<a href="chapter7.html#table-7-1">表格 7.1</a>)。我们必须这么做,因为浏览器无法直接访问 <code>update</code> 动作,必须先提交编辑表单,所以 Capybara 也做不到。访问编辑资料页面只能测试 <code>edit</code> 动作是否有权限继续操作,而不能测试 <code>update</code> 动作的授权情况。所以,如果要测试 <code>update</code> 动作是否有权限进行操作只能直接发送 <code>PATCH</code> 请求。(你可能已经猜到了,除了 <code>patch</code> 方法之外,Rails 中的测试还支持 <code>get</code>、<code>post</code> 和 <code>delete</code> 方法。)</p>
<p>直接发送某种 HTTP 请求时,我们需要处理更底层的 <code>response</code> 对象。和 Capybara 提供的 <code>page</code> 对象不同,我们可以使用 <code>response</code> 测试服务器的响应。本例我们检测了 <code>update</code> 动作的响应是否转向了登录页面:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">signin_path</span><span class="p">)</span> <span class="p">}</span>
</pre></div>
</div>
<div class="figure" id="figure-9-5">
<img src="figures/signin_page_protected_mockup_bootstrap.png" alt="signin page protected mockup bootstrap" />
<p class="caption"><span>图 9.5:</span>访问受保护页面转向后的页面构思图</p>
</div>
<p>我们要使用 <code>before_action</code> 方法实现权限限制,这个方法会在指定的动作执行之前,先运行指定的方法。为了实现要求用户先登录的限制,我们要定义一个名为 <code>signed_in_user</code> 的方法,然后调用 <code>before_action :signed_in_user</code>,如代码 9.12 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-12"><p class="caption"><span>代码 9.12:</span>添加 <code>signed_in_user</code> 事前过滤器</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">private</span>
<span class="k">def</span> <span class="nf">user_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span>
<span class="ss">:password_confirmation</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Before filters</span>
<span class="k">def</span> <span class="nf">signed_in_user</span>
<span class="n">redirect_to</span> <span class="n">signin_url</span><span class="p">,</span> <span class="ss">notice: </span><span class="s2">"Please sign in."</span> <span class="k">unless</span> <span class="n">signed_in?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>默认情况下,事前过滤器会应用于控制器中的所有动作,所以在上述代码中我们传入了 <code>:only</code> 参数指定只应用在 <code>edit</code> 和 <code>update</code> 动作上。</p>
<div class="figure" id="figure-9-6">
<img src="figures/protected_sign_in_bootstrap.png" alt="protected sign in bootstrap" />
<p class="caption"><span>图 9.6:</span>尝试访问受保护的页面后显示的登录表单</p>
</div>
<p>注意,在代码 9.12 中我们使用了设定 <code>flash[:notice]</code> 的简便方式,把 <code>redirect_to</code> 方法的第二个参数指定为一个 Hash。这段代码等同于:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">unless</span> <span class="n">signed_in?</span>
<span class="n">flash</span><span class="p">[</span><span class="ss">:notice</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Please sign in."</span>
<span class="n">redirect_to</span> <span class="n">signin_url</span>
<span class="k">end</span>
</pre></div>
</div>
<p>(<code>flash[:error]</code> 也可以使用上述的简便方式,但 <code>flash[:success]</code> 却不可以。)</p>
<p><code>flash[:notice]</code> 加上 <code>flash[:success]</code> 和 <code>flash[:error]</code> 就是我们要介绍的三种 Flash 消息,Bootstrap 为这三种消息都提供了样式。退出后再尝试访问 /users/1/edit,就会看到如图 9.6 所示的黄色提示框。</p>
<p>现在所有的测试应该都可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h3 id='section-9-2-2'><span>9.2.2</span> 用户只能编辑自己的资料</h3>
<p>当然,要求用户必须先登录还是不够的,用户必须只能编辑自己的资料。我们的测试可以这么编写,用其他用户的身份登录,然后访问 <code>edit</code> 和 <code>update</code> 动作,如代码 9.13 所示。注意,这段测试我们不用 Capybara(<code>no_capybara: true</code>),而使用 <code>get</code> 和 <code>patch</code> 方法直接访问 <code>edit</code> 和 <code>update</code> 动作。而且,用户不应该尝试编辑其他用户的资料,我们没有转向登录页面,而是转到了网站的首页。</p>
<div class="codeblock has-caption" id="codeblock-9-13"><p class="caption"><span>代码 9.13:</span>测试只有自己才能访问 <code>edit</code> 和 <code>update</code> 动作</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"authorization"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"as wrong user"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:wrong_user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">sign_in</span> <span class="n">user</span><span class="p">,</span> <span class="ss">no_capybara: </span><span class="kp">true</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"submitting a GET request to the Users#edit action"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">get</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">wrong_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">).</span><span class="nf">not_to</span> <span class="n">match</span><span class="p">(</span><span class="n">full_title</span><span class="p">(</span><span class="s1">'Edit user'</span><span class="p">))</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">root_url</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"submitting a PATCH request to the Users#update action"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">patch</span> <span class="n">user_path</span><span class="p">(</span><span class="n">wrong_user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">specify</span> <span class="p">{</span> <span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">root_url</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>注意,创建预构件的方法还可以接受第二个参数:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span>
</pre></div>
</div>
<p>上述代码会用指定的 Email 替换默认值,然后创建用户。我们的测试要确保其他的用户不能访问原来那个用户的 <code>edit</code> 和 <code>update</code> 动作。</p>
<p>我们在控制器中加入了第二个事前过滤器,调用 <code>correct_user</code> 方法,如代码 9.14 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-14"><p class="caption"><span>代码 9.14:</span>保护 <code>edit</code> 和 <code>update</code> 动作的 <code>correct_user</code> 事前过滤器</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">correct_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">edit</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">update_attributes</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="n">flash</span><span class="p">[</span><span class="ss">:success</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Profile updated"</span>
<span class="n">redirect_to</span> <span class="vi">@user</span>
<span class="k">else</span>
<span class="n">render</span> <span class="s1">'edit'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">private</span>
<span class="k">def</span> <span class="nf">user_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span>
<span class="ss">:password_confirmation</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Before filters</span>
<span class="k">def</span> <span class="nf">signed_in_user</span>
<span class="n">redirect_to</span> <span class="n">signin_url</span><span class="p">,</span> <span class="ss">notice: </span><span class="s2">"Please sign in."</span> <span class="k">unless</span> <span class="n">signed_in?</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">correct_user</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="n">redirect_to</span><span class="p">(</span><span class="n">root_path</span><span class="p">)</span> <span class="k">unless</span> <span class="n">current_user?</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>上述代码中的 <code>correct_user</code> 方法使用了 <code>current_user?</code> 方法,我们要在 Sessions 帮助方法模块中定义一下,如代码 9.15。</p>
<div class="codeblock has-caption" id="codeblock-9-15"><p class="caption"><span>代码 9.15:</span>定义 <code>current_user?</code> 方法</p><p class="file"><code>app/helpers/sessions_helper.rb</code></p><div class="highlight type-ruby"><pre><span class="k">module</span> <span class="nn">SessionsHelper</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">current_user</span>
<span class="n">remember_token</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">hash</span><span class="p">(</span><span class="n">cookies</span><span class="p">[</span><span class="ss">:remember_token</span><span class="p">])</span>
<span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">remember_token: </span><span class="n">remember_token</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">current_user?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">user</span> <span class="o">==</span> <span class="n">current_user</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>代码 9.14 同时也更新了 <code>edit</code> 和 <code>update</code> 动作的代码。之前在代码 9.2 中,我们是这样写的:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="k">def</span> <span class="nf">edit</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
</pre></div>
</div>
<p><code>update</code> 代码类似。既然 <code>correct_user</code> 事前过滤器中已经定义了 <code>@user</code>,这两个动作中就不再需要再定义 <code>@user</code> 变量了。</p>
<p>在继续阅读之前,你应该验证一下测试是否可以通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<h3 id='section-9-2-3'><span>9.2.3</span> 更友好的转向</h3>
<p>程序的权限限制基本完成了,但是还有一点小小的不足:不管用户尝试访问的是哪个受保护的页面,登录后都会转向资料页面。也就是说,如果未登录的用户访问了编辑资料页面,会要求先登录,登录转到的页面是 /users/1,而不是 <code>/users/1/edit</code>。如果登录后能转到用户之前想访问的页面就更好了。</p>
<p>针对这种更友好的转向,我们可以这样编写测试,先访问编辑用户资料页面,转向登录页面后,填写正确的登录信息,点击“Sign in”按钮,然后显示的应该是编辑用户资料页面,而不是用户资料页面。相应的测试如代码 9.16 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-16"><p class="caption"><span>代码 9.16:</span>测试更友好的转向</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"authorization"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"for non-signed-in users"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"when attempting to visit a protected page"</span> <span class="k">do</span>
<span class="n">before</span> <span class="k">do</span>
<span class="n">visit</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">fill_in</span> <span class="s2">"Email"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">user</span><span class="p">.</span><span class="nf">email</span>
<span class="n">fill_in</span> <span class="s2">"Password"</span><span class="p">,</span> <span class="ss">with: </span><span class="n">user</span><span class="p">.</span><span class="nf">password</span>
<span class="n">click_button</span> <span class="s2">"Sign in"</span>
<span class="k">end</span>
<span class="n">describe</span> <span class="s2">"after signing in"</span> <span class="k">do</span>
<span class="n">it</span> <span class="s2">"should render the desired protected page"</span> <span class="k">do</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_title</span><span class="p">(</span><span class="s1">'Edit user'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>下面我们来实现这个设想。<sup class="footnote" id="fnref-9-4"><a href="#fn-9-4" rel="footnote">4</a></sup>要转向用户真正想访问的页面,我们要在某个地方存储这个页面的地址,登录后再转向这个页面。我们要通过两个方法来实现这个过程,<code>store_location</code> 和 <code>redirect_back_or</code>,都在 Sessions 帮助方法模块中定义,如代码 9.17。</p>
<div class="codeblock has-caption" id="codeblock-9-17"><p class="caption"><span>代码 9.17:</span>实现更友好的转向所需的代码</p><p class="file"><code>app/helpers/sessions_helper.rb</code></p><div class="highlight type-ruby"><pre><span class="k">module</span> <span class="nn">SessionsHelper</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">redirect_back_or</span><span class="p">(</span><span class="n">default</span><span class="p">)</span>
<span class="n">redirect_to</span><span class="p">(</span><span class="n">session</span><span class="p">[</span><span class="ss">:return_to</span><span class="p">]</span> <span class="o">||</span> <span class="n">default</span><span class="p">)</span>
<span class="n">session</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:return_to</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">store_location</span>
<span class="n">session</span><span class="p">[</span><span class="ss">:return_to</span><span class="p">]</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">fullpath</span> <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="nf">get?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>地址的存储使用了 Rails 提供的 <code>session</code>,<code>session</code> 可以理解成和 <a href="chapter8.html#section-8-2-1">8.2.1 节</a>中介绍的 <code>cookies</code> 是类似的东西,会在浏览器关闭后自动失效。(在 <a href="chapter8.html#section-8-5">8.5 节</a>中介绍过,其实 <code>session</code> 的实现方法正是如此。)我们还使用了 <code>request</code> 对象的 <code>fullpath</code> 方法获取了所请求页面的完整地址。在 <code>store_location</code> 方法中,把完整的请求地址存储在 <code>session[:return_to]</code> 中。但这个方法只能在 <code>GET</code> 请求中使用(<code>if request.get?</code>)。这么做,当未登录的用户提交表单时,不会存储转向地址(这种情况虽然很罕见,但在提交表单前,如果用户手动删除了记忆权标,还是会发生的),那么本来期望接收 <code>POST</code>、<code>PATCH</code> 或 <code>DELETE</code> 请求的动作实际收到的却是 <code>GET</code> 请求,就会产生异常。<sup class="footnote" id="fnref-9-5"><a href="#fn-9-5" rel="footnote">5</a></sup></p>
<p>要使用 <code>store_location</code>,我们要把它加入 <code>signed_in_user</code> 事前过滤器中,如代码 9.18 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-18"><p class="caption"><span>代码 9.18:</span>把 <code>store_location</code> 加入 <code>signed_in_user</code> 事前过滤器</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">correct_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">edit</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">private</span>
<span class="k">def</span> <span class="nf">user_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">,</span>
<span class="ss">:password_confirmation</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># Before filters</span>
<span class="k">def</span> <span class="nf">signed_in_user</span>
<span class="k">unless</span> <span class="n">signed_in?</span>
<span class="n">store_location</span>
<span class="n">redirect_to</span> <span class="n">signin_url</span><span class="p">,</span> <span class="ss">notice: </span><span class="s2">"Please sign in."</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">correct_user</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="n">redirect_to</span><span class="p">(</span><span class="n">root_path</span><span class="p">)</span> <span class="k">unless</span> <span class="n">current_user?</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>实现转向操作,要在 Sessions 控制器的 <code>create</code> 动作中加入 <code>redirect_back_or</code> 方法,用户登录后转到适当的页面,如代码 9.19 所示。如果存储了之前请求的地址,<code>redirect_back_or</code> 方法就会转向这个地址,否则会转向参数中指定的地址。</p>
<div class="codeblock has-caption" id="codeblock-9-19"><p class="caption"><span>代码 9.19:</span>加入友好转向后的 <code>create</code> 动作</p><p class="file"><code>app/controllers/sessions_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">create</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">params</span><span class="p">[</span><span class="ss">:session</span><span class="p">][</span><span class="ss">:email</span><span class="p">].</span><span class="nf">downcase</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span> <span class="o">&&</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:session</span><span class="p">][</span><span class="ss">:password</span><span class="p">])</span>
<span class="n">sign_in</span> <span class="n">user</span>
<span class="n">redirect_back_or</span> <span class="n">user</span>
<span class="k">else</span>
<span class="n">flash</span><span class="p">.</span><span class="nf">now</span><span class="p">[</span><span class="ss">:error</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'Invalid email/password combination'</span>
<span class="n">render</span> <span class="s1">'new'</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>(如果你做了第 8 章的练习 1,记得把代码 9.19 中的 <code>params</code> Hash 换掉。)</p>
<p><code>redirect_back_or</code> 方法在下面这行代码中使用了“或”操作符 <code>||</code>:</p>
<div class="codeblock"><div class="highlight type-ruby"><pre><span class="n">session</span><span class="p">[</span><span class="ss">:return_to</span><span class="p">]</span> <span class="o">||</span> <span class="n">default</span>
</pre></div>
</div>
<p>如果 <code>session[:return_to]</code> 的值不是 <code>nil</code>,上面这行代码就会返回 <code>session[:return_to]</code> 的值,否则会返回 <code>default</code>。注意,在代码 9.17 中,成功转向后就会删除存储在 session 中的转向地址。如果不删除的话,在关闭浏览器之前,每次登录后都会转到存储的地址上。(对这一过程的测试留作练习,参见 <a href="chapter9.html#section-9-6">9.6 节</a>。)</p>
<p>加入上述代码之后,代码 9.16 中对友好转向的集成测试应该可以通过了。至此,我们也就完成了基本的用户身份验证和页面保护机制。和之前一样,在继续阅读之前,最好确认一下所有的测试是否都可以通过:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<div class="figure" id="figure-9-7">
<img src="figures/user_index_mockup_bootstrap.png" alt="user index mockup bootstrap" />
<p class="caption"><span>图 9.7:</span>用户列表页面的构思图,包含了分页链接和“Users”导航链接</p>
</div>
<h2 id='section-9-3'><span>9.3</span> 列出所有用户</h2>
<p>本节我们要添加计划中的倒数第二个用户动作,<code>index</code>。<code>index</code> 动作不会显示某一个用户,而是显示所有的用户。在这个过程中,我们要学习如何在数据库中生成示例用户数据,以及如何分页显示用户列表,显示任意数量的用户。用户列表、分页链接和“所有用户(Users)”导航链接的构思图如图 9.7 所示。<sup class="footnote" id="fnref-9-6"><a href="#fn-9-6" rel="footnote">6</a></sup>在 <a href="chapter9.html#section-9-4">9.4 节</a> 中,我们还会在用户列表中添加删除链接,这样就可以删除有问题的用户了。</p>
<h3 id='section-9-3-1'><span>9.3.1</span> 用户列表</h3>
<p>单个用户的资料页面是对外开放的,不过用户列表页面只有注册用户才能访问。我们先来编写测试。在测试中我们要检测 <code>index</code> 动作是被保护的,如果访问 <code>users_path</code> 会转向登录页面。和其他的权限限制测试一样,我们也会把这个测试放在身份验证的集成测试中,如代码 9.20 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-20"><p class="caption"><span>代码 9.20:</span>测试 <code>index</code> 动作是否是被保护的</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"authorization"</span> <span class="k">do</span>
<span class="n">describe</span> <span class="s2">"for non-signed-in users"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"in the Users controller"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"visiting the user index"</span> <span class="k">do</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">visit</span> <span class="n">users_path</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="s1">'Sign in'</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>若要这个测试通过,我们要把 <code>index</code> 动作加入 <code>signed_in_user</code> 事前过滤器,如代码 9.21 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-21"><p class="caption"><span>代码 9.21:</span>访问 <code>index</code> 动作必须先登录</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:index</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">correct_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>接下来,我们要测试用户登录后,用户列表页面要有特定的标题和内容,还要列出网站中所有的用户。为此,我们要创建三个用户预构件,以第一个用户的身份登录,然后检测用户列表页面中是否有一个列表,各用户的名字都包含在一个单独的 <code>li</code> 标签中。注意,我们要为每个用户分配不同的名字,这样列表中的用户才是不一样的,如代码 9.22 所示。</p>
<div class="codeblock has-caption" id="codeblock-9-22"><p class="caption"><span>代码 9.22:</span>用户列表页面的测试</p><p class="file"><code>spec/requests/user_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"User pages"</span> <span class="k">do</span>
<span class="n">subject</span> <span class="p">{</span> <span class="n">page</span> <span class="p">}</span>
<span class="n">describe</span> <span class="s2">"index"</span> <span class="k">do</span>
<span class="n">before</span> <span class="k">do</span>
<span class="n">sign_in</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span>
<span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Bob"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Ben"</span><span class="p">,</span> <span class="ss">email: </span><span class="s2">"[email protected]"</span><span class="p">)</span>
<span class="n">visit</span> <span class="n">users_path</span>
<span class="k">end</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="s1">'All users'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_content</span><span class="p">(</span><span class="s1">'All users'</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="s2">"should list each user"</span> <span class="k">do</span>
<span class="no">User</span><span class="p">.</span><span class="nf">all</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">user</span><span class="o">|</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_selector</span><span class="p">(</span><span class="s1">'li'</span><span class="p">,</span> <span class="ss">text: </span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>你可能还记得,我们在演示程序的相关代码中介绍过(参见代码 2.4),在程序中我们可以使用 <code>User.all</code> 从数据库中取回所有的用户,赋值给实例变量 <code>@users</code> 在视图中使用,如代码 9.23 所示。(你可能会觉得一次列出所有的用户不太好,你是对的,我们会在 <a href="chapter9.html#section-9-3-3">9.3.3 节</a>中改进。)</p>
<div class="codeblock has-caption" id="codeblock-9-23"><p class="caption"><span>代码 9.23:</span>Users 控制器的 <code>index</code> 动作</p><p class="file"><code>app/controllers/users_controller.rb</code></p><div class="highlight type-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="p">:</span><span class="n">signed_in_user</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:index</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="p">]</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">def</span> <span class="n">index</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">all</span>
<span class="k">end</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
</pre></div>
</div>
<p>要显示用户列表页面,我们要创建一个视图,遍历所有的用户,把单个用户包含在 <code>li</code> 标签中。我们要使用 <code>each</code> 方法遍历所有用户,显示用户的 Gravatar 头像和名字,然后把所有的用户包含在无序列表 <code>ul</code> 标签中,如代码 9.24 所示。在代码 9.24 中,我们用到了 <a href="chapter7.html#section-7-6">7.6 节</a>练习中代码 7.30 的成果,允许向 Gravatar 帮助方法传入第二个参数,指定头像的大小。如果你之前没有做这个练习题,在继续阅读之前请参照代码 7.30 更新 Users 控制器的帮助方法文件。</p>
<div class="codeblock has-caption" id="codeblock-9-24"><p class="caption"><span>代码 9.24:</span>用户列表页面的视图</p><p class="file"><code>app/views/users/index.html.erb</code></p><div class="highlight type-erb"><pre><span class="cp"><%</span> <span class="n">provide</span><span class="p">(</span><span class="ss">:title</span><span class="p">,</span> <span class="s1">'All users'</span><span class="p">)</span> <span class="cp">%></span>
<span class="nt"><h1></span>All users<span class="nt"></h1></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"users"</span><span class="nt">></span>
<span class="cp"><%</span> <span class="vi">@users</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">user</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><li></span>
<span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="n">user</span><span class="p">,</span> <span class="ss">size: </span><span class="mi">52</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="n">user</span> <span class="cp">%></span>
<span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></ul></span>
</pre></div>
</div>
<p>我们再添加一些 CSS(更确切的说是 SCSS)美化一下,如代码 9.25。</p>
<div class="codeblock has-caption" id="codeblock-9-25"><p class="caption"><span>代码 9.25:</span>用户列表页面的 CSS</p><p class="file"><code>app/assets/stylesheets/custom.css.scss</code></p><div class="highlight type-scss"><pre><span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">/*</span> <span class="nt">Users</span> <span class="nt">index</span> <span class="o">*/</span>
<span class="nc">.users</span> <span class="p">{</span>
<span class="nl">list-style</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nt">li</span> <span class="p">{</span>
<span class="nl">overflow</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">border-top</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nv">$grayLighter</span><span class="p">;</span>
<span class="k">&</span><span class="nd">:last-child</span> <span class="p">{</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nv">$grayLighter</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
</div>
<p>最后,我们还要在头部的导航中加入到用户列表页面的链接,链接的地址为 <code>users_path</code>,这是<a href="chapter7.html#table-7-1">表格 7.1</a>中还没介绍的最后一个具名路由了。相应的测试(代码 9.26)和程序所需的代码(代码 9.27)都很简单。</p>
<div class="codeblock has-caption" id="codeblock-9-26"><p class="caption"><span>代码 9.26:</span>检测“Users”链接的测试</p><p class="file"><code>spec/requests/authentication_pages_spec.rb</code></p><div class="highlight type-ruby"><pre><span class="nb">require</span> <span class="s1">'spec_helper'</span>
<span class="n">describe</span> <span class="s2">"Authentication"</span> <span class="k">do</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">describe</span> <span class="s2">"with valid information"</span> <span class="k">do</span>
<span class="n">let</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">{</span> <span class="no">FactoryGirl</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="p">}</span>
<span class="n">before</span> <span class="p">{</span> <span class="n">sign_in</span> <span class="n">user</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_title</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="nf">name</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Users'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">users_path</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Profile'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Settings'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">edit_user_path</span><span class="p">(</span><span class="n">user</span><span class="p">))</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Sign out'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">signout_path</span><span class="p">)</span> <span class="p">}</span>
<span class="n">it</span> <span class="p">{</span> <span class="n">should_not</span> <span class="n">have_link</span><span class="p">(</span><span class="s1">'Sign in'</span><span class="p">,</span> <span class="ss">href: </span><span class="n">signin_path</span><span class="p">)</span> <span class="p">}</span>
<span class="p">.</span>
<span class="nf">.</span>
<span class="p">.</span>
<span class="nf">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="codeblock has-caption" id="codeblock-9-27"><p class="caption"><span>代码 9.27:</span>添加“Users”链接</p><p class="file"><code>app/views/layouts/_header.html.erb</code></p><div class="highlight type-erb"><pre><span class="nt"><header</span> <span class="na">class=</span><span class="s">"navbar navbar-fixed-top navbar-inverse"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"navbar-inner"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"sample app"</span><span class="p">,</span> <span class="n">root_path</span><span class="p">,</span> <span class="ss">id: </span><span class="s2">"logo"</span> <span class="cp">%></span>
<span class="nt"><nav></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"nav pull-right"</span><span class="nt">></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Home"</span><span class="p">,</span> <span class="n">root_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Help"</span><span class="p">,</span> <span class="n">help_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="n">signed_in?</span> <span class="cp">%></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Users"</span><span class="p">,</span> <span class="n">users_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">id=</span><span class="s">"fat-menu"</span> <span class="na">class=</span><span class="s">"dropdown"</span><span class="nt">></span>
<span class="nt"><a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"dropdown-toggle"</span> <span class="na">data-toggle=</span><span class="s">"dropdown"</span><span class="nt">></span>
Account <span class="nt"><b</span> <span class="na">class=</span><span class="s">"caret"</span><span class="nt">></b></span>
<span class="nt"></a></span>
<span class="nt"><ul</span> <span class="na">class=</span><span class="s">"dropdown-menu"</span><span class="nt">></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Profile"</span><span class="p">,</span> <span class="n">current_user</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Settings"</span><span class="p">,</span> <span class="n">edit_user_path</span><span class="p">(</span><span class="n">current_user</span><span class="p">)</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"divider"</span><span class="nt">></li></span>
<span class="nt"><li></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign out"</span><span class="p">,</span> <span class="n">signout_path</span><span class="p">,</span> <span class="ss">method: </span><span class="s2">"delete"</span> <span class="cp">%></span>
<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign in"</span><span class="p">,</span> <span class="n">signin_path</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></ul></span>
<span class="nt"></nav></span>
<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"></header></span>
</pre></div>
</div>
<p>至此,用户列表页面的功能就实现了,所有的测试也都可以通过了:</p>
<div class="codeblock"><div class="highlight type-shell"><pre><span class="gp">$ </span>bundle <span class="nb">exec </span>rspec spec/
</pre></div>
</div>
<p>不过,如图 9.8 所示,页面中只显示了一个用户,有点孤单单。下面,让我们来改变一下这种悲惨状况。</p>
<h3 id='section-9-3-2'><span>9.3.2</span> 示例用户</h3>
<p>在本小节中,我们要为应用程序添加更多的用户。如果要让用户列表看上去像个列表,我们可以在浏览器中访问注册页面,然后一个一个地注册用户,不过还有更好的方法,让 Ruby 和 Rake 为我们创建用户。</p>
<p>首先,我们要在 <code>Gemfile</code> 中加入 <code>faker</code>(如代码 9.28 所示),使用这个 gem,我们可以使用半真实的名字和 Email 地址创建示例用户。</p>
<div class="codeblock has-caption" id="codeblock-9-28"><p class="caption"><span>代码 9.28:</span>把 faker 加入 <code>Gemfile</code></p><div class="highlight type-ruby"><pre><span class="n">source</span> <span class="s1">'https://rubygems.org'</span>