-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathchapter12.html
1871 lines (1615 loc) · 179 KB
/
chapter12.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>Ruby on Rails 教程 - 第 12 章 关注用户</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="最好的 Ruby on Rails 入门教程"/>
<meta name="keywords" content="ruby, rails, tutorial"/>
<meta name="author" content="Michael Hartl"/>
<meta name="translator" content="安道"/>
<meta name="generator" content="persie 0.0.1.beta.3"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="http://cdn.staticfile.org/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="assets/style.css"/>
<script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.staticfile.org/twitter-bootstrap/3.2.0/js/collapse.min.js"></script>
<script type="text/javascript" src="assets/global.js"></script>
</head>
<body>
<header class="navbar navbar-default navbar-fixed-top navbar-book">
<div class="container">
<div class="navbar-header">
<a href="http://railstutorial-china.org" class="navbar-brand">Ruby on Rails 教程</a>
<button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target=".book-navbar-collapse">
<span class="sr-only">导航</span>
<i class="fa fa-bars"></i>
</button>
<a href="http://railstutorial-china.org/#purchase" id="navbar-purchase-xs" class="btn btn-warning navbar-btn visible-xs collapsed-purchase-btn">购买</a>
</div>
<nav class="collapse navbar-collapse book-navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="http://railstutorial-china.org" title="首页">首页</a></li>
<li class="active"><a href="http://railstutorial-china.org/read/" title="在线阅读">阅读</a></li>
<li><a href="http://railstutorial-china.org/blog/" title="最新消息">博客</a></li>
<li><a href="https://selfstore.io/products/189/topics" title="论坛">论坛</a></li>
<li class="hidden-xs"><div><a href="http://railstutorial-china.org/#purchase" id="navbar-purchase" class="btn btn-warning navbar-btn" title="购买电子书">购买</a></div></li>
</ul>
</nav>
</div>
</header>
<div class="content">
<div class="container">
<div class="row">
<div class="col-lg-offset-2 col-lg-8">
<div class="alert alert-warning">
<p>在线版的内容可能落后于电子书,如果想及时获得更新,请<a href="http://railstutorial-china.org/#purchase" title="购买电子书">购买电子书</a>。</p>
</div>
<article class="article">
<section data-type="chapter" id="following-users">
<h1><span class="title-label">第 12 章</span> 关注用户</h1>
<p>这一章,我们要为演示应用添加社交功能,允许用户关注(及取消关注)其他人,并在主页显示被关注用户发布的微博。我们会在 <a href="#the-relationship-model">12.1 节</a>学习如何建立用户之间的关系,然后在 <a href="#a-web-interface-for-following-users">12.2 节</a>编写相应的网页界面(还会介绍 Ajax)。最后,在 <a href="#the-status-feed">12.3 节</a>实现功能完善的动态流。</p>
<p>这是本书最后一章,有些内容具有挑战性,比如说,为了实现动态流,我们会使用一些 Ruby 和 SQL 技巧。 通过这些示例,你会了解到 Rails 是如何处理更加复杂的数据模型的,这些知识也会在你日后开发其他应用时发挥作用。 为了帮助你平稳地从学习过渡到独立开发,<a href="#following-users-conclusion">12.4 节</a>介绍了一些进阶学习资源。</p>
<p>因为本章的内容比较有挑战性,所以在开始编写代码之前,我们先来讨论一下界面。 和之前的章节一样,在开发之前,我们要使用构思图。<sup>[<a id="fn-ref-1" href="#fn-1">1</a>]</sup>完整的页面流程是这样的:一个用户 (John Calvin) 从他的资料页面(<a href="#fig-page-flow-profile-mockup">图 12.1</a>)浏览到用户列表页面(<a href="#fig-page-flow-user-index-mockup">图 12.2</a>),关注了另一个用户;然后他又打开另一个用户 Thomas Hobbes 的资料页面(<a href="#fig-page-flow-other-profile-follow-button">图 12.3</a>),点击“Follow”(关注)按钮 关注了他,这时“Follow”按钮会变为“Unfollow”(取消关注),而且关注 Hobbes 的人数增加了一个(<a href="#fig-page-flow-other-profile-unfollow-button-mockup">图 12.4</a>);接着,Calvin 回到主页,看到他关注的人数也增加了一个,而且在动态流中能看到 Hobbes 发布的微博(<a href="#fig-page-flow-home-page-feed-mockup">图 12.5</a>)。本章接下来的内容就是要实现这样的页面流程。</p>
<div id="fig-page-flow-profile-mockup" class="figure"><img src="images/chapter12/page_flow_profile_mockup_3rd_edition.png" alt="page flow profile mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 12.1</span>:一个用户的资料页面</div></div>
<div id="fig-page-flow-user-index-mockup" class="figure"><img src="images/chapter12/page_flow_user_index_mockup_bootstrap.png" alt="page flow user index mockup bootstrap" /><div class="figcaption"><span class="title-label">图 12.2</span>:找一个想关注的用户</div></div>
<div id="fig-page-flow-other-profile-follow-button" class="figure"><img src="images/chapter12/page_flow_other_profile_follow_button_mockup_3rd_edition.png" alt="page flow other profile follow button mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 12.3</span>:想关注的那个用户的资料页面,有一个“Follow”(关注)按钮</div></div>
<div id="fig-page-flow-other-profile-unfollow-button-mockup" class="figure"><img src="images/chapter12/page_flow_other_profile_unfollow_button_mockup_3rd_edition.png" alt="page flow other profile unfollow button mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 12.4</span>:资料页面中显示了“Unfollow”(取消关注)按钮,而且关注他的人数增加了一个</div></div>
<div id="fig-page-flow-home-page-feed-mockup" class="figure"><img src="images/chapter12/page_flow_home_page_feed_mockup_3rd_edition.png" alt="page flow home page feed mockup 3rd edition" /><div class="figcaption"><span class="title-label">图 12.5</span>:首页,显示了动态流,而且关注的人数增加了一个</div></div>
<section data-type="sect1" id="the-relationship-model">
<h1><span class="title-label">12.1</span> “关系”模型</h1>
<p>为了实现用户关注功能,首先要创建一个看上去并不是那么直观的数据模型。一开始我们可能会认为 <code>has_many</code> 关联能满足我们的要求:一个用户关注多个用户,而且也被多个用户关注。但实际上这种实现方式有问题,下面我们会学习如何使用 <code>has_many :through</code> 解决。</p>
<p>和之前一样,如果使用 Git,现在应该新建一个主题分支:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>git checkout master
<span class="nv">$ </span>git checkout -b following-users
</pre></div>
</div>
<section data-type="sect2" id="a-problem-with-the-data-model-and-a-solution">
<h2><span class="title-label">12.1.1</span> 数据模型带来的问题以及解决方法</h2>
<p>在构建关注用户所需的数据模型之前,我们先来分析一个典型的案例。假如一个用户关注了另外一个用户,比如 Calvin 关注了 Hobbes,也就是 Hobbes 被 Calvin 关注了,那么 Calvin 就是“关注人”(follower),Hobbes 则是“被关注人”(followed)。按照 Rails 默认的复数命名习惯, 我们称关注了某个用户的所有用户为这个用户的“followers”,因此,<code>hobbes.followers</code> 是一个数组,包含所有关注了 Hobbes 的用户。不过,如果顺序颠倒,这种表述就说不通了:默认情况下,所有被关注的用户应该叫“followeds”,但是这样说并不符合英语语法。所以,参照 Twitter 的叫法,我们把被关注的用户叫做“following”(例如,“50 following, 75 followers”)。因此,Calvin 关注的人可以通过 <code>calvin.following</code> 数组获取。</p>
<p>经过上述讨论,我们可以按照<a href="#fig-naive-user-has-many-following">图 12.6</a> 中的方式构建被关注用户的模型——一个 <code>following</code> 表和 <code>has_many</code> 关联。由于 <code>user.following</code> 应该是一个用户对象组成的数组,所以 <code>following</code> 表中的每一行都应该是一个用户,通过 <code>followed_id</code> 列标识。然后再通过 <code>follower_id</code> 列建立关联。<sup>[<a id="fn-ref-2" href="#fn-2">2</a>]</sup>除此之外,由于每一行都是一个用户,所以还要在表中加入用户的其他属性,例如名字、电子邮件地址和密码等。</p>
<div id="fig-naive-user-has-many-following" class="figure"><img src="images/chapter12/naive_user_has_many_following.png" alt="naive user has many following" /><div class="figcaption"><span class="title-label">图 12.6</span>:用户关注的人(天真方式)</div></div>
<p><a href="#fig-naive-user-has-many-following">图 12.6</a> 中的数据模型有个问题——存在非常多的冗余,每一行不仅包括了被关注用户的 ID,还包括了他们的其他信息,而这些信息在 <code>users</code> 表中都有。 更糟糕的是,为了保存关注我的人,还需要另一个同样冗余的 <code>followers</code> 表。这么做会导致数据模型极难维护:用户修改名字时,不仅要修改 <code>users</code> 表中的数据,还要修改 <code>following</code> 和 <code>followers</code> 表中包含这个用户的每一个记录。</p>
<p>造成这个问题的原因是缺少了一层抽象。找到合适的抽象有一种方法:思考在应用中如何实现关注用户的操作。<a href="chapter7.html#a-users-resource">7.1.2 节</a>介绍过,REST 架构涉及到资源的创建和销毁两个操作。 由此引出了两个问题:用户关注另一个用户时,创建了什么?用户取消关注另一个用户时,销毁了什么?按照这样的方式思考,我们会发现,在关注用户的过程中,创建和销毁的是两个用户之间的“关系”。因此,一个用户有多个“关系”,从而通过这个“关系”得到很多我关注的人(<code>following</code>)和关注我的人(<code>followers</code>)。</p>
<p>在实现应用的数据模型时还有一个细节要注意:Facebook 实现的关系是对称的,A 关注 B 时,B 也就关注了 A;而我们要实现的关系和 Twitter 类似,是不对称的,Calvin 可以关注 Hobbes,但 Hobbes 并不需要关注 Calvin。为了区分这两种情况,我们要使用专业的术语:如果 Calvin 关注了 Hobbes,但 Hobbes 没有关注 Calvin,那么 Calvin 和 Hobbes 之间建立的是“主动关系”(Active Relationship),而 Hobbes 和 Calvin 之间是“被动关系”(Positive Relationship)。<sup>[<a id="fn-ref-3" href="#fn-3">3</a>]</sup></p>
<p>现在我们集中精力实现“主动关系”,即获取我关注的用户。<a href="#followers">12.1.5 节</a>会实现“被动关系”。从<a href="#fig-naive-user-has-many-following">图 12.6</a> 中可以看出实现的方式:既然我关注的每一个用户都由 <code>followed_id</code> 独一无二的标识出来了,我们就可以把 <code>following</code> 表转化成 <code>active_relationships</code> 表,删掉用户的属性,然后使用 <code>followed_id</code> 从 <code>users</code> 表中获取我关注的用户的信息。这个数据模型如<a href="#fig-user-has-many-following">图 12.7</a> 所示。</p>
<div id="fig-user-has-many-following" class="figure"><img src="images/chapter12/user_has_many_following_3rd_edition.png" alt="user has many following 3rd edition" /><div class="figcaption"><span class="title-label">图 12.7</span>:通过“主动关系”获取我关注的用户</div></div>
<p>因为“主动关系”和“被动关系”最终会存储在同一个表中,所以我们把这个表命名为“relationships”。这个表对应的模型是 <code>Relationship</code>,如<a href="#fig-relationship-model">图 12.8</a> 所示。从 <a href="#followed-users">12.1.4 节</a>开始,我们会介绍如何使用这个模型同时实现“主动关系”和“被动关系”。</p>
<div id="fig-relationship-model" class="figure"><img src="images/chapter12/relationship_model.png" alt="relationship model" /><div class="figcaption"><span class="title-label">图 12.8</span>:Relationship 数据模型</div></div>
<p>为此,我们要生成所需的模型:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate model Relationship follower_id:integer followed_id:integer
</pre></div>
</div>
<p>因为我们会通过 <code>follower_id</code> 和 <code>followed_id</code> 查找关系,所以还要为这两个列建立索引,提高查询的效率,如<a href="#listing-relationships-migration">代码清单 12.1</a> 所示。</p>
<div id="listing-relationships-migration" data-type="listing">
<h5><span class="title-label">代码清单 12.1</span>:在 <code>relationships</code> 表中添加索引</h5>
<div class="source-file">db/migrate/[timestamp]_create_relationships.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">CreateRelationships</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:relationships</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="o">.</span><span class="n">integer</span> <span class="ss">:follower_id</span>
<span class="n">t</span><span class="o">.</span><span class="n">integer</span> <span class="ss">:followed_id</span>
<span class="n">t</span><span class="o">.</span><span class="n">timestamps</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="hll"> <span class="n">add_index</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="ss">:follower_id</span>
</span><span class="hll"> <span class="n">add_index</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="ss">:followed_id</span>
</span><span class="hll"> <span class="n">add_index</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="o">[</span><span class="ss">:follower_id</span><span class="p">,</span> <span class="ss">:followed_id</span><span class="o">]</span><span class="p">,</span> <span class="ss">unique</span><span class="p">:</span> <span class="kp">true</span>
</span> <span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>在<a href="#listing-relationships-migration">代码清单 12.1</a> 中,我们还设置了一个“多键索引”,确保 (<code>follower_id, followed_id</code>) 组合是唯一的,避免多次关注同一个用户。(可以和<a href="chapter6.html#listing-email-uniqueness-index">代码清单 6.28</a> 中保持电子邮件地址唯一的索引比较一下。)从 <a href="#followed-users">12.1.4 节</a>起会看到,用户界面不会允许这样的事发生,但添加索引后,如果用户试图创建重复的关系(例如使用 <code>curl</code> 这样的命令行工具),应用会抛出异常。</p>
<p>为了创建 <code>relationships</code> 表,和之前一样,我们要执行迁移:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate
</pre></div>
</div>
</section>
<section data-type="sect2" id="user-relationship-associations">
<h2><span class="title-label">12.1.2</span> 用户和“关系”模型之间的关联</h2>
<p>在获取我关注的人和关注我的人之前,我们要先建立用户和“关系”模型之间的关联。一个用户有多个“关系”(<code>has_many</code>), 因为一个“关系”涉及到两个用户,所以“关系”同时属于(<code>belongs_to</code>)该用户和被关注的用户。</p>
<p>和 <a href="chapter11.html#user-micropost-associations">11.1.3 节</a>创建时微博一样,我们要通过关联创建“关系”,如下面的代码所示:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">user</span><span class="o">.</span><span class="n">active_relationships</span><span class="o">.</span><span class="n">build</span><span class="p">(</span><span class="ss">followed_id</span><span class="p">:</span> <span class="o">.</span><span class="n">.</span><span class="o">.</span><span class="p">)</span>
</pre></div>
</div>
<p>此时,你可能想在应用中加入类似于 <a href="chapter11.html#user-micropost-associations">11.1.3 节</a>使用的代码。我们要添加的代码确实很像,但有两处不同。</p>
<p>首先,把用户和微博关联起来时我们写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="ss">:microposts</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>之所以可以这么写,是因为 Rails 会寻找 <code>:microposts</code> 符号对应的模型,即 <code>Micropost</code>。<sup>[<a id="fn-ref-4" href="#fn-4">4</a>]</sup>可是现在模型名为 <code>Relationship</code>,而我们想写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">has_many</span> <span class="ss">:active_relationships</span>
</pre></div>
</div>
<p>所以要告诉 Rails 模型的类名。</p>
<p>其次,前面在微博模型中是这么写的:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">Micropost</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>之所以可以这么写,是因为 <code>microposts</code> 表中有识别用户的 <code>user_id</code> 列(<a href="chapter11.html#the-basic-model">11.1.1 节</a>)。这种连接两个表的列,我们称之为“外键”(foreign key)。当指向用户模型的外键为 <code>user_id</code> 时,Rails 会自动获知关联,因为默认情况下,Rails 会寻找名为 <code><class>_id</code> 的外键,其中 <code><class></code> 是模型类名的小写形式。<sup>[<a id="fn-ref-5" href="#fn-5">5</a>]</sup>现在,尽管我们处理的还是用户,但识别用户使用的外键是 <code>follower_id</code>,所以要告诉 Rails 这一变化。</p>
<p>综上所述,用户和“关系”模型之间的关联如<a href="#listing-user-relationships-association">代码清单 12.2</a> 和<a href="#listing-relationship-belongs-to">代码清单 12.3</a> 所示。</p>
<div id="listing-user-relationships-association" data-type="listing">
<h5><span class="title-label">代码清单 12.2</span>:实现“主动关系”中的 <code>has_many</code> 关联</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"Relationship"</span><span class="p">,</span>
</span><span class="hll"> <span class="ss">foreign_key</span><span class="p">:</span> <span class="s2">"follower_id"</span><span class="p">,</span>
</span><span class="hll"> <span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
</span> <span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>(因为删除用户时也要删除涉及这个用户的“关系”,所以我们在关联中加入了 <code>dependent: :destroy</code>。)</p>
<div id="listing-relationship-belongs-to" data-type="listing">
<h5><span class="title-label">代码清单 12.3</span>:在“关系”模型中添加 <code>belongs_to</code> 关联</h5>
<div class="source-file">app/models/relationship.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">Relationship</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="hll"> <span class="n">belongs_to</span> <span class="ss">:follower</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"User"</span>
</span><span class="hll"> <span class="n">belongs_to</span> <span class="ss">:followed</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"User"</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p>尽管 <a href="#followers">12.1.5 节</a>才会用到 <code>followed</code> 关联,但同时添加易于理解。</p>
<p>建立上述关联后,会得到一系列类似于<a href="chapter11.html#table-association-methods">表 11.1</a> 中的方法,如<a href="#table-association-methods-relationships">表 12.1</a> 所示。</p>
<table id="table-association-methods-relationships" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 12.1</span>:用户和“主动关系”关联后得到的方法简介</caption>
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">方法</th>
<th class="tableblock halign-left valign-top">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>active_relationship.follower</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">获取关注我的用户</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>active_relationship.followed</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">获取我关注的用户</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.active_relationships.create(followed_id: other_user.id)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建 <code>user</code> 发起的“主动关系”</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.active_relationships.create!(followed_id: other_user.id)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">创建 <code>user</code> 发起的“主动关系”(失败时抛出异常)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>user.active_relationships.build(followed_id: other_user.id)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">构建 <code>user</code> 发起的“主动关系”对象</p></td>
</tr>
</tbody>
</table>
</section>
<section data-type="sect2" id="validations">
<h2><span class="title-label">12.1.3</span> 数据验证</h2>
<p>在继续之前,我们要在“关系”模型中添加一些验证。测试(<a href="#listing-relationship-validation-tests">代码清单 12.4</a>)和应用代码(<a href="#listing-relationship-validations">代码清单 12.5</a>)都非常直观。和生成的用户固件一样(<a href="chapter6.html#listing-default-fixtures">代码清单 6.29</a>),生成的“关系”固件也违背了迁移中的唯一性约束(<a href="#listing-relationships-migration">代码清单 12.1</a>)。这个问题的解决方法也和之前一样(<a href="chapter6.html#listing-empty-fixtures">代码清单 6.30</a>)——删除自动生成的固件,如<a href="#listing-empty-relationship-fixture">代码清单 12.6</a> 所示。</p>
<div id="listing-relationship-validation-tests" data-type="listing">
<h5><span class="title-label">代码清单 12.4</span>:测试“关系”模型中的验证</h5>
<div class="source-file">test/models/relationship_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">RelationshipTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@relationship</span> <span class="o">=</span> <span class="no">Relationship</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">follower_id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">followed_id</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should be valid"</span> <span class="k">do</span>
<span class="n">assert</span> <span class="vi">@relationship</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should require a follower_id"</span> <span class="k">do</span>
<span class="vi">@relationship</span><span class="o">.</span><span class="n">follower_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@relationship</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should require a followed_id"</span> <span class="k">do</span>
<span class="vi">@relationship</span><span class="o">.</span><span class="n">followed_id</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="n">assert_not</span> <span class="vi">@relationship</span><span class="o">.</span><span class="n">valid?</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<div id="listing-relationship-validations" data-type="listing">
<h5><span class="title-label">代码清单 12.5</span>:在“关系”模型中添加验证</h5>
<div class="source-file">app/models/relationship.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">Relationship</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">belongs_to</span> <span class="ss">:follower</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"User"</span>
<span class="n">belongs_to</span> <span class="ss">:followed</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"User"</span>
<span class="hll"> <span class="n">validates</span> <span class="ss">:follower_id</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span><span class="hll"> <span class="n">validates</span> <span class="ss">:followed_id</span><span class="p">,</span> <span class="ss">presence</span><span class="p">:</span> <span class="kp">true</span>
</span><span class="k">end</span>
</pre></div>
</div>
<div id="listing-empty-relationship-fixture" data-type="listing">
<h5><span class="title-label">代码清单 12.6</span>:删除“关系”固件</h5>
<div class="source-file">test/fixtures/relationships.yml</div>
<div class="highlight language-yaml"><pre><span class="c1"># empty</span>
</pre></div>
</div>
<p>现在,测试应该可以通过:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 12.7</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
<section data-type="sect2" id="followed-users">
<h2><span class="title-label">12.1.4</span> 我关注的用户</h2>
<p>现在到“关系”的核心部分了——获取我关注的用户(<code>following</code>)和关注我的用户(<code>followers</code>)。这里我们要首次用到 <code>has_many :through</code> 关联:用户通过“关系”模型关注了多个用户,如<a href="#fig-user-has-many-following">图 12.7</a> 所示。默认情况下,在 <code>has_many :through</code> 关联中,Rails 会寻找关联名单数形式对应的外键。例如:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">has_many</span> <span class="ss">:followeds</span><span class="p">,</span> <span class="ss">through</span><span class="p">:</span> <span class="ss">:active_relationships</span>
</pre></div>
</div>
<p>Rails 发现关联名是“followeds”,把它变成单数形式“followed”,因此会在 <code>relationships</code> 表中获取一个由 <code>followed_id</code> 组成的集合。不过,<a href="#a-problem-with-the-data-model-and-a-solution">12.1.1 节</a>说过,写成 <code>user.followeds</code> 有点说不通,所以我们会使用 <code>user.following</code>。Rails 允许定制默认生成的关联方法:使用 <code>source</code> 参数指定 <code>following</code> 数组由 <code>followed_id</code> 组成,如<a href="#listing-has-many-following-through-active-relationships">代码清单 12.8</a> 所示。</p>
<div id="listing-has-many-following-through-active-relationships" data-type="listing">
<h5><span class="title-label">代码清单 12.8</span>:在用户模型中添加 <code>following</code> 关联</h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
<span class="n">has_many</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"Relationship"</span><span class="p">,</span>
<span class="ss">foreign_key</span><span class="p">:</span> <span class="s2">"follower_id"</span><span class="p">,</span>
<span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">through</span><span class="p">:</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="ss">:followed</span>
</span> <span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>定义这个关联后,我们可以充分利用 Active Record 和数组的功能。例如,可以使用 <code>include?</code> 方法(<a href="chapter4.html#arrays-and-ranges">4.3.1 节</a>)检查我关注的用户中有没有某个用户,或者通过关联查找一个用户:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">user</span><span class="o">.</span><span class="n">following</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="n">user</span><span class="o">.</span><span class="n">following</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
</pre></div>
</div>
<p>很多情况下我们都可以把 <code>following</code> 当成数组来用,Rails 会使用特定的方式处理 <code>following</code>,所以这么做很高效。例如:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">following</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
</pre></div>
</div>
<p>看起来好像是要把我关注的所有用户都从数据库中读取出来,然后再调用 <code>include?</code>。其实不然,为了提高效率,Rails 会直接在数据库层执行相关的操作。(和 <a href="chapter11.html#rendering-microposts">11.2.1 节</a>使用 <code>user.microposts.count</code> 获取数量一样,都直接在数据库中操作。)</p>
<p>为了处理关注用户的操作,我们要定义两个辅助方法:<code>follow</code> 和 <code>unfollow</code>。这样我们就可以写 <code>user.follow(other_user)</code>。我们还要定义 <code>following?</code> 布尔值方法,检查一个用户是否关注了另一个用户。<sup>[<a id="fn-ref-6" href="#fn-6">6</a>]</sup></p>
<p>现在是编写测试的好时机,因为我们还要等很久才会开发关注用户的网页界面,如果一直没人监管,很难向前推进。我们可以为用户模型编写一个简短的测试,先调用 <code>following?</code> 方法确认某个用户没有关注另一个用户,然后调用 <code>follow</code> 方法关注这个用户,再使用 <code>following?</code> 方法确认关注成功了,最后调用 <code>unfollow</code> 方法取消关注,并确认操作成功,如<a href="#listing-utility-method-tests">代码清单 12.9</a> 所示。</p>
<div id="listing-utility-method-tests" data-type="listing">
<h5><span class="title-label">代码清单 12.9</span>:测试关注用户相关的几个辅助方法 <span class="red">RED</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"should follow and unfollow a user"</span> <span class="k">do</span>
<span class="n">michael</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="n">archer</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">)</span>
<span class="hll"> <span class="n">assert_not</span> <span class="n">michael</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
</span><span class="hll"> <span class="n">michael</span><span class="o">.</span><span class="n">follow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
</span><span class="hll"> <span class="n">assert</span> <span class="n">michael</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
</span><span class="hll"> <span class="n">michael</span><span class="o">.</span><span class="n">unfollow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
</span><span class="hll"> <span class="n">assert_not</span> <span class="n">michael</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>参照<a href="#table-association-methods-relationships">表 12.1</a>,我们要使用 <code>following</code> 关联定义 <code>follow</code>、<code>unfollow</code> 和 <code>following?</code> 方法,如<a href="#listing-follow-unfollow-following">代码清单 12.10</a> 所示。(注意,只要可能,我们就省略 <code>self</code>。)</p>
<div id="listing-follow-unfollow-following" data-type="listing">
<h5><span class="title-label">代码清单 12.10</span>:定义关注用户相关的几个辅助方法 <span class="green">GREEN</span></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">def</span> <span class="nf">feed</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
<span class="c1"># 关注另一个用户</span>
<span class="k">def</span> <span class="nf">follow</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="hll"> <span class="n">active_relationships</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="ss">followed_id</span><span class="p">:</span> <span class="n">other_user</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="c1"># 取消关注另一个用户</span>
<span class="k">def</span> <span class="nf">unfollow</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="hll"> <span class="n">active_relationships</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="ss">followed_id</span><span class="p">:</span> <span class="n">other_user</span><span class="o">.</span><span class="n">id</span><span class="p">)</span><span class="o">.</span><span class="n">destroy</span>
</span> <span class="k">end</span>
<span class="c1"># 如果当前用户关注了指定的用户,返回 true</span>
<span class="k">def</span> <span class="nf">following?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
<span class="hll"> <span class="n">following</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">other_user</span><span class="p">)</span>
</span> <span class="k">end</span>
<span class="kp">private</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>现在,测试能通过了:</p>
<div data-type="listing">
<h5><span class="title-label">代码清单 12.11</span>:<strong class="green">GREEN</strong></h5>
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
<section data-type="sect2" id="followers">
<h2><span class="title-label">12.1.5</span> 关注我的人</h2>
<p>“关系”的最后一部分是定义与 <code>user.following</code> 对应的 <code>user.followers</code> 方法。从<a href="#fig-user-has-many-following">图 12.7</a> 中得知,获取关注我的人所需的数据都已经存在于 <code>relationships</code> 表中(我们要参照<a href="#listing-user-relationships-association">代码清单 12.2</a> 中实现 <code>active_relationships</code> 表的方式)。其实我们要使用的方法和实现我关注的人一样,只要对调 <code>follower_id</code> 和 <code>followed_id</code> 的位置,并把 <code>active_relationships</code> 换成 <code>passive_relationships</code> 即可,如<a href="#fig-user-has-many-followers">图 12.9</a> 所示。</p>
<div id="fig-user-has-many-followers" class="figure"><img src="images/chapter12/user_has_many_followers_3rd_edition.png" alt="user has many followers 3rd edition" /><div class="figcaption"><span class="title-label">图 12.9</span>:通过“被动关系”获取关注我的用户</div></div>
<p>参照<a href="#listing-has-many-following-through-active-relationships">代码清单 12.8</a>,我们可以使用<a href="#listing-has-many-following-through-passive-relationships">代码清单 12.12</a> 中的代码实现<a href="#fig-user-has-many-followers">图 12.9</a> 中的模型。</p>
<div id="listing-has-many-following-through-passive-relationships" data-type="listing">
<h5><span class="title-label">代码清单 12.12</span>:使用“被动关系”实现 <code>user.followers</code></h5>
<div class="source-file">app/models/user.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">has_many</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
<span class="n">has_many</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"Relationship"</span><span class="p">,</span>
<span class="ss">foreign_key</span><span class="p">:</span> <span class="s2">"follower_id"</span><span class="p">,</span>
<span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:passive_relationships</span><span class="p">,</span> <span class="ss">class_name</span><span class="p">:</span> <span class="s2">"Relationship"</span><span class="p">,</span>
</span><span class="hll"> <span class="ss">foreign_key</span><span class="p">:</span> <span class="s2">"followed_id"</span><span class="p">,</span>
</span><span class="hll"> <span class="ss">dependent</span><span class="p">:</span> <span class="ss">:destroy</span>
</span> <span class="n">has_many</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">through</span><span class="p">:</span> <span class="ss">:active_relationships</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="ss">:followed</span>
<span class="hll"> <span class="n">has_many</span> <span class="ss">:followers</span><span class="p">,</span> <span class="ss">through</span><span class="p">:</span> <span class="ss">:passive_relationships</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="ss">:follower</span>
</span> <span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>值得注意的是,其实我们可以省略 <code>followers</code> 关联中的 <code>source</code> 参数,直接写成:</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">has_many</span> <span class="ss">:followers</span><span class="p">,</span> <span class="ss">through</span><span class="p">:</span> <span class="ss">:passive_relationships</span>
</pre></div>
</div>
<p>因为 Rails 会把“followers”转换成单数“follower”,然后查找名为 <code>follower_id</code> 的外键。<a href="#listing-has-many-following-through-passive-relationships">代码清单 12.12</a> 之所以保留了 <code>source</code> 参数,是为了和 <code>has_many :following</code> 关联的结构保持一致。</p>
<p>我们可以使用 <code>followers.include?</code> 测试这个数据模型,如<a href="#listing-followers-test">代码清单 12.13</a> 所示。(这段测试本可以使用与 <code>following?</code> 方法对应的 <code>followed_by?</code> 方法,但应用中用不到,所以没这么做。)</p>
<div id="listing-followers-test" data-type="listing">
<h5><span class="title-label">代码清单 12.13</span>:测试 <code>followers</code> 关联 <span class="green">GREEN</span></h5>
<div class="source-file">test/models/user_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UserTest</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">TestCase</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"should follow and unfollow a user"</span> <span class="k">do</span>
<span class="n">michael</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="n">archer</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">)</span>
<span class="n">assert_not</span> <span class="n">michael</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">michael</span><span class="o">.</span><span class="n">follow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">assert</span> <span class="n">michael</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="hll"> <span class="n">assert</span> <span class="n">archer</span><span class="o">.</span><span class="n">followers</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">michael</span><span class="p">)</span>
</span> <span class="n">michael</span><span class="o">.</span><span class="n">unfollow</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="n">assert_not</span> <span class="n">michael</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="n">archer</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>我们只在<a href="#listing-utility-method-tests">代码清单 12.9</a> 的基础上增加了一行代码,但若想让这个测试通过,很多事情都要正确处理才行,所以足以测试<a href="#listing-has-many-following-through-passive-relationships">代码清单 12.12</a> 中的关联。</p>
<p>现在,整个测试组件都能通过:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake <span class="nb">test</span>
</pre></div>
</div>
</section>
</section>
<section data-type="sect1" id="a-web-interface-for-following-users">
<h1><span class="title-label">12.2</span> 关注用户的网页界面</h1>
<p><a href="#the-relationship-model">12.1 节</a>用到了很多数据模型技术,可能要花些时间才能完全理解。其实,理解这些关联最好的方式是在网页界面中使用。</p>
<p>在本章的导言中,我们介绍了关注用户的操作流程。本节,我们要实现这些构思的页面,以及关注和取消关注功能。我们还会创建两个页面,分别列出我关注的用户和关注我的用户。在 <a href="#the-status-feed">12.3 节</a>,我们会实现用户的动态流,届时,这个演示应用才算完成。</p>
<section data-type="sect2" id="sample-following-data">
<h2><span class="title-label">12.2.1</span> 示例数据</h2>
<p>和之前的几章一样,我们要使用 Rake 任务把“关系”相关的种子数据加载到数据库中。有了示例数据,我们就可以先实现网页界面,本节末尾再实现后端功能。</p>
<p>“关系”相关的种子数据如<a href="#listing-sample-relationships">代码清单 12.14</a> 所示。我们让第一个用户关注第 3-51 个用户,并让第 4-41 个用户关注第一个用户。这样的数据足够用来开发应用的界面了。</p>
<div id="listing-sample-relationships" data-type="listing">
<h5><span class="title-label">代码清单 12.14</span>:在种子数据中添加“关系”相关的数据</h5>
<div class="source-file">db/seeds.rb</div>
<div class="highlight language-yaml"><pre><span class="c1"># Users</span>
<span class="l-Scalar-Plain">User.create!(name</span><span class="p-Indicator">:</span> <span class="s">"Example</span><span class="nv"> </span><span class="s">User"</span><span class="err">,</span>
<span class="l-Scalar-Plain">email</span><span class="p-Indicator">:</span> <span class="s">"[email protected]"</span><span class="err">,</span>
<span class="l-Scalar-Plain">password</span><span class="p-Indicator">:</span> <span class="s">"foobar"</span><span class="err">,</span>
<span class="l-Scalar-Plain">password_confirmation</span><span class="p-Indicator">:</span> <span class="s">"foobar"</span><span class="err">,</span>
<span class="l-Scalar-Plain">admin</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">true,</span>
<span class="l-Scalar-Plain">activated</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">true,</span>
<span class="l-Scalar-Plain">activated_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">Time.zone.now)</span>
<span class="l-Scalar-Plain">99.times do |n|</span>
<span class="l-Scalar-Plain">name = Faker::Name.name</span>
<span class="l-Scalar-Plain">email = "example-#{n+1}@railstutorial.org"</span>
<span class="l-Scalar-Plain">password = "password"</span>
<span class="l-Scalar-Plain">User.create!(name</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">name,</span>
<span class="l-Scalar-Plain">email</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">email,</span>
<span class="l-Scalar-Plain">password</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">password,</span>
<span class="l-Scalar-Plain">password_confirmation</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">password,</span>
<span class="l-Scalar-Plain">activated</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">true,</span>
<span class="l-Scalar-Plain">activated_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">Time.zone.now)</span>
<span class="l-Scalar-Plain">end</span>
<span class="c1"># Microposts</span>
<span class="l-Scalar-Plain">users = User.order(:created_at).take(6)</span>
<span class="l-Scalar-Plain">50.times do</span>
<span class="l-Scalar-Plain">content = Faker::Lorem.sentence(5)</span>
<span class="l-Scalar-Plain">users.each { |user| user.microposts.create!(content</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">content) }</span>
<span class="l-Scalar-Plain">end</span>
<span class="hll"><span class="c1"># Following relationships</span>
</span><span class="hll"><span class="l-Scalar-Plain">users = User.all</span>
</span><span class="hll"><span class="l-Scalar-Plain">user = users.first</span>
</span><span class="hll"><span class="l-Scalar-Plain">following = users[2..50]</span>
</span><span class="hll"><span class="l-Scalar-Plain">followers = users[3..40]</span>
</span><span class="hll"><span class="l-Scalar-Plain">following.each { |followed| user.follow(followed) }</span>
</span><span class="hll"><span class="l-Scalar-Plain">followers.each { |follower| follower.follow(user) }</span>
</span></pre></div>
</div>
<p>然后像之前一样,执行下面的命令,运行<a href="#listing-sample-relationships">代码清单 12.14</a> 中的代码:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:migrate:reset
<span class="nv">$ </span>bundle <span class="nb">exec </span>rake db:seed
</pre></div>
</div>
</section>
<section data-type="sect2" id="stats-and-a-follow-form">
<h2><span class="title-label">12.2.2</span> 数量统计和关注表单</h2>
<p>现在示例用户已经关注了其他用户,也被其他用户关注了,我们要更新一下用户资料页面和首页,把这些变动显示出来。首先,我们要创建一个局部视图,在资料页面和首页显示我关注的人和关注我的人的数量。然后再添加关注和取消关注表单,并且在专门的页面中列出我关注的用户和关注我的用户。</p>
<p><a href="#a-problem-with-the-data-model-and-a-solution">12.1.1 节</a>说过,我们参照了 Twitter 的叫法,在我关注的用户数量后使用“following”作标记(label),例如“50 following”。<a href="#fig-page-flow-profile-mockup">图 12.1</a> 中的构思图就使用了这种表述方式,现在把这部分单独摘出来,如<a href="#fig-stats-partial-mockup">图 12.10</a> 所示。</p>
<div id="fig-stats-partial-mockup" class="figure"><img src="images/chapter12/stats_partial_mockup.png" alt="stats partial mockup" /><div class="figcaption"><span class="title-label">图 12.10</span>:数量统计局部视图的构思图</div></div>
<p><a href="#fig-stats-partial-mockup">图 12.10</a> 中显示的数量统计包含当前用户关注的人数和关注当前用户的人数,而且分别链接到专门的用户列表页面。在<a href="chapter5.html#filling-in-the-layout">第 5 章</a>,我们使用 <code>#</code> 占位符代替真实的网址,因为那时我们还没怎么接触路由。现在,虽然 <a href="#following-and-followers-pages">12.2.3 节</a>才会创建所需的页面,不过可以先设置路由,如<a href="#listing-following-followers-actions-routes">代码清单 12.15</a> 所示。这段代码在 <code>resources</code> 块中使用了 <code>:member</code> 方法。我们以前没用过这个方法,你可以猜测一下这个方法的作用是什么。</p>
<div id="listing-following-followers-actions-routes" data-type="listing">
<h5><span class="title-label">代码清单 12.15</span>:在用户控制器中添加 <code>following</code> 和 <code>followers</code> 两个动作</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">routes</span><span class="o">.</span><span class="n">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'help'</span> <span class="o">=></span> <span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'about'</span> <span class="o">=></span> <span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'contact'</span> <span class="o">=></span> <span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'signup'</span> <span class="o">=></span> <span class="s1">'users#new'</span>
<span class="n">get</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#new'</span>
<span class="n">post</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#create'</span>
<span class="n">delete</span> <span class="s1">'logout'</span> <span class="o">=></span> <span class="s1">'sessions#destroy'</span>
<span class="hll"> <span class="n">resources</span> <span class="ss">:users</span> <span class="k">do</span>
</span><span class="hll"> <span class="n">member</span> <span class="k">do</span>
</span><span class="hll"> <span class="n">get</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">:followers</span>
</span><span class="hll"> <span class="k">end</span>
</span><span class="hll"> <span class="k">end</span>
</span> <span class="n">resources</span> <span class="ss">:account_activations</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:edit</span><span class="o">]</span>
<span class="n">resources</span> <span class="ss">:password_resets</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="o">]</span>
<span class="n">resources</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="o">]</span>
<span class="k">end</span>
</pre></div>
</div>
<p>你可能猜到了,设定上述路由后,得到的 URL 地址类似 /users/1/following 和 /users/1/followers 这种形式。不错,<a href="#listing-following-followers-actions-routes">代码清单 12.15</a> 的作用确实如此。因为这两个页面都是用来显示数据的,所以我们使用了 <code>get</code> 方法,指定这两个地址响应的是 GET 请求。而且,使用 <code>member</code> 方法后,这两个动作对应的 URL 地址中都会包含用户的 ID。除此之外,我们还可以使用 <code>collection</code> 方法,但 URL 中就没有用户 ID 了。所以,如下的代码</p>
<div data-type="listing">
<div class="highlight language-ruby"><pre><span class="n">resources</span> <span class="ss">:users</span> <span class="k">do</span>
<span class="n">collection</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:tigers</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>得到的 URL 是 /users/tigers(或许可以用来显示应用中所有的老虎)。<sup>[<a id="fn-ref-7" href="#fn-7">7</a>]</sup></p>
<p><a href="#listing-following-followers-actions-routes">代码清单 12.15</a> 生成的路由如<a href="#table-following-routes">表 12.2</a> 所示。留意一下我关注的用户页面和关注我的用户页面的具名路由是什么,稍后会用到。</p>
<table id="table-following-routes" class="tableblock frame-all grid-all" style="width: 100%;">
<caption><span class="title-label">表 12.2</span>:<a href="#listing-following-followers-actions-routes">代码清单 12.15</a> 中设置的规则生成的 REST 路由</caption>
<colgroup>
<col style="width: 15%;" />
<col style="width: 20%;" />
<col style="width: 15%;" />
<col style="width: 50%;" />
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">HTTP 请求</th>
<th class="tableblock halign-left valign-top">URL</th>
<th class="tableblock halign-left valign-top">动作</th>
<th class="tableblock halign-left valign-top">具名路由</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">GET</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1/following</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>following</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>following_user_path(1)</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">GET</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">/users/1/followers</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>followers</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>followers_user_path(1)</code></p></td>
</tr>
</tbody>
</table>
<p>设好了路由后,我们来编写数量统计局部视图。我们要在一个 <code>div</code> 元素中显示几个链接,如<a href="#listing-stats-partial">代码清单 12.16</a> 所示。</p>
<div id="listing-stats-partial" data-type="listing">
<h5><span class="title-label">代码清单 12.16</span>:显示数量统计的局部视图</h5>
<div class="source-file">app/views/shared/_stats.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%</span> <span class="vi">@user</span> <span class="o">||=</span> <span class="n">current_user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"><div class="stats"></span>
<span class="x"> <a href="</span><span class="cp"><%=</span> <span class="n">following_user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span><span class="x">"></span>
<span class="x"> <strong id="following" class="stat"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">following</span><span class="o">.</span><span class="n">count</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </strong></span>
<span class="x"> following</span>
<span class="x"> </a></span>
<span class="x"> <a href="</span><span class="cp"><%=</span> <span class="n">followers_user_path</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span><span class="x">"></span>
<span class="x"> <strong id="followers" class="stat"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">followers</span><span class="o">.</span><span class="n">count</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </strong></span>
<span class="x"> followers</span>
<span class="x"> </a></span>
<span class="x"></div></span>
</pre></div>
</div>
<p>因为用户资料页面和首页都要使用这个局部视图,所以在<a href="#listing-stats-partial">代码清单 12.16</a> 的第一行,我们要获取正确的用户对象:</p>
<div data-type="listing">
<div class="highlight language-erb"><pre><span class="cp"><%</span> <span class="vi">@user</span> <span class="o">||=</span> <span class="n">current_user</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>我们在<a href="chapter8.html#aside-or-equals">旁注 8.1</a>中介绍过这种用法,如果 <code>@user</code> 不是 <code>nil</code>(在用户资料页面),这行代码没什么效果;如果是 <code>nil</code>(在首页),就会把当前用户赋值给 <code>@user</code>。还有一处要注意,我关注的人数和关注我的人数是通过关联获取的,分别使用 <code>@user.following.count</code> 和 <code>@user.followers.count</code>。</p>
<p>我们可以和<a href="chapter11.html#listing-user-show-microposts">代码清单 11.23</a> 中获取微博数量的代码对比一下,微博的数量通过 <code>@user.microposts.count</code> 获取。为了提高效率,Rails 会直接在数据库层统计数量。</p>
<p>最后还有一个细节需要注意,某些元素指定了 CSS ID,例如:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><span class="nt"><strong</span> <span class="na">id=</span><span class="s">"following"</span> <span class="na">class=</span><span class="s">"stat"</span><span class="nt">></span>
...
<span class="nt"></strong></span>
</pre></div>
</div>
<p>这些 ID 是为 <a href="#a-working-follow-button-with-ajax">12.2.5 节</a>中的 Ajax 准备的,因为 Ajax 要通过独一无二的 ID 获取页面中的元素。</p>
<p>编写好局部视图,把它放入首页就很简单了,如<a href="#listing-home-page-stats">代码清单 12.17</a> 所示。</p>
<div id="listing-home-page-stats" data-type="listing">
<h5><span class="title-label">代码清单 12.17</span>:在首页显示数量统计</h5>
<div class="source-file">app/views/static_pages/home.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%</span> <span class="k">if</span> <span class="n">logged_in?</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <div class="row"></span>
<span class="x"> <aside class="col-md-4"></span>
<span class="x"> <section class="user_info"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/user_info'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </section></span>
<span class="hll"><span class="x"> <section class="stats"></span>
</span><span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</span> <span class="cp">%></span><span class="x"></span>
</span><span class="hll"><span class="x"> </section></span>
</span><span class="x"> <section class="micropost_form"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/micropost_form'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </section></span>
<span class="x"> </aside></span>
<span class="x"> <div class="col-md-8"></span>
<span class="x"> <h3>Micropost Feed</h3></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/feed'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="x"> </div></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> .</span>
<span class="x"> .</span>
<span class="x"> .</span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>我们要添加一些 SCSS 代码,美化数量统计,如<a href="#listing-stats-css">代码清单 12.18</a> 所示(包含本章用到的所有样式)。添加样式后,首页如<a href="#fig-home-page-follow-stats">图 12.11</a> 所示。</p>
<div id="listing-stats-css" data-type="listing">
<h5><span class="title-label">代码清单 12.18</span>:首页侧边栏的 SCSS 样式</h5>
<div class="source-file">app/assets/stylesheets/custom.css.scss</div>
<div class="highlight language-scss"><pre><span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="o">/*</span> <span class="nt">sidebar</span> <span class="o">*/</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.gravatar</span> <span class="p">{</span>
<span class="na">float</span><span class="o">:</span> <span class="no">left</span><span class="p">;</span>
<span class="na">margin-right</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.gravatar_edit</span> <span class="p">{</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">15</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="hll"><span class="nc">.stats</span> <span class="p">{</span>
</span> <span class="na">overflow</span><span class="o">:</span> <span class="no">auto</span><span class="p">;</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="nt">a</span> <span class="p">{</span>
<span class="na">float</span><span class="o">:</span> <span class="no">left</span><span class="p">;</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">0</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
<span class="na">border-left</span><span class="o">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="no">solid</span> <span class="nv">$gray-lighter</span><span class="p">;</span>
<span class="na">color</span><span class="o">:</span> <span class="nb">gray</span><span class="p">;</span>
<span class="k">&</span><span class="nd">:first-child</span> <span class="p">{</span>
<span class="na">padding-left</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="na">border</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">&</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="na">text-decoration</span><span class="o">:</span> <span class="no">none</span><span class="p">;</span>
<span class="na">color</span><span class="o">:</span> <span class="nb">blue</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nt">strong</span> <span class="p">{</span>
<span class="na">display</span><span class="o">:</span> <span class="no">block</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="hll"><span class="nc">.user_avatars</span> <span class="p">{</span>
</span> <span class="na">overflow</span><span class="o">:</span> <span class="no">auto</span><span class="p">;</span>
<span class="na">margin-top</span><span class="o">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
<span class="nc">.gravatar</span> <span class="p">{</span>
<span class="na">margin</span><span class="o">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="mi">1</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">a</span> <span class="p">{</span>
<span class="na">padding</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="hll"><span class="nc">.users.follow</span> <span class="p">{</span>
</span> <span class="na">padding</span><span class="o">:</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="cm">/* forms */</span>
<span class="nc">.</span>
<span class="nc">.</span>
<span class="nc">.</span>
</pre></div>
</div>
<div id="fig-home-page-follow-stats" class="figure"><img src="images/chapter12/home_page_follow_stats_3rd_edition.png" alt="home page follow stats 3rd edition" /><div class="figcaption"><span class="title-label">图 12.11</span>:显示有数量统计的首页</div></div>
<p>稍后再把数量统计局部视图添加到用户资料页面中,现在先来编写关注和取消关注按钮的局部视图,如<a href="#listing-follow-form-partial">代码清单 12.19</a> 所示。</p>
<div id="listing-follow-form-partial" data-type="listing">
<h5><span class="title-label">代码清单 12.19</span>:显示关注或取消关注表单的局部视图</h5>
<div class="source-file">app/views/users/_follow_form.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%</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="cp">%></span><span class="x"></span>
<span class="x"> <div id="follow_form"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">if</span> <span class="n">current_user</span><span class="o">.</span><span class="n">following?</span><span class="p">(</span><span class="vi">@user</span><span class="p">)</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'unfollow'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'follow'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>这段代码其实也没做什么,只是把具体的工作分配给 <code>follow</code> 和 <code>unfollow</code> 局部视图了。我们要再次设置路由,加入“关系”资源,如<a href="#listing-relationships-resource">代码清单 12.20</a> 所示,和微博资源的设置类似(<a href="chapter11.html#listing-microposts-resource">代码清单 11.29</a>)。</p>
<div id="listing-relationships-resource" data-type="listing">
<h5><span class="title-label">代码清单 12.20</span>:添加“关系”资源的路由设置</h5>
<div class="source-file">config/routes.rb</div>
<div class="highlight language-ruby"><pre><span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">routes</span><span class="o">.</span><span class="n">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'static_pages#home'</span>
<span class="n">get</span> <span class="s1">'help'</span> <span class="o">=></span> <span class="s1">'static_pages#help'</span>
<span class="n">get</span> <span class="s1">'about'</span> <span class="o">=></span> <span class="s1">'static_pages#about'</span>
<span class="n">get</span> <span class="s1">'contact'</span> <span class="o">=></span> <span class="s1">'static_pages#contact'</span>
<span class="n">get</span> <span class="s1">'signup'</span> <span class="o">=></span> <span class="s1">'users#new'</span>
<span class="n">get</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#new'</span>
<span class="n">post</span> <span class="s1">'login'</span> <span class="o">=></span> <span class="s1">'sessions#create'</span>
<span class="n">delete</span> <span class="s1">'logout'</span> <span class="o">=></span> <span class="s1">'sessions#destroy'</span>
<span class="n">resources</span> <span class="ss">:users</span> <span class="k">do</span>
<span class="n">member</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:following</span><span class="p">,</span> <span class="ss">:followers</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">resources</span> <span class="ss">:account_activations</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:edit</span><span class="o">]</span>
<span class="n">resources</span> <span class="ss">:password_resets</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:new</span><span class="p">,</span> <span class="ss">:create</span><span class="p">,</span> <span class="ss">:edit</span><span class="p">,</span> <span class="ss">:update</span><span class="o">]</span>
<span class="n">resources</span> <span class="ss">:microposts</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="o">]</span>
<span class="hll"> <span class="n">resources</span> <span class="ss">:relationships</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</span><span class="ss">:create</span><span class="p">,</span> <span class="ss">:destroy</span><span class="o">]</span>
</span><span class="k">end</span>
</pre></div>
</div>
<p><code>follow</code> 和 <code>unfollow</code> 局部视图的代码分别如<a href="#listing-follow-form">代码清单 12.21</a> 和<a href="#listing-unfollow-form">代码清单 12.22</a> 所示。</p>
<div id="listing-follow-form" data-type="listing">
<h5><span class="title-label">代码清单 12.21</span>:关注用户的表单</h5>
<div class="source-file">app/views/users/_follow.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="n">current_user</span><span class="o">.</span><span class="n">active_relationships</span><span class="o">.</span><span class="n">build</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="x"></span>
<span class="x"> <div></span><span class="cp"><%=</span> <span class="n">hidden_field_tag</span> <span class="ss">:followed_id</span><span class="p">,</span> <span class="vi">@user</span><span class="o">.</span><span class="n">id</span> <span class="cp">%></span><span class="x"></div></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">submit</span> <span class="s2">"Follow"</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s2">"btn btn-primary"</span> <span class="cp">%></span><span class="x"></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<div id="listing-unfollow-form" data-type="listing">
<h5><span class="title-label">代码清单 12.22</span>:取消关注用户的表单</h5>
<div class="source-file">app/views/users/_unfollow.html.erb</div>
<div class="highlight language-erb"><pre><span class="cp"><%=</span> <span class="n">form_for</span><span class="p">(</span><span class="n">current_user</span><span class="o">.</span><span class="n">active_relationships</span><span class="o">.</span><span class="n">find_by</span><span class="p">(</span><span class="ss">followed_id</span><span class="p">:</span> <span class="vi">@user</span><span class="o">.</span><span class="n">id</span><span class="p">),</span>
<span class="ss">html</span><span class="p">:</span> <span class="p">{</span> <span class="nb">method</span><span class="p">:</span> <span class="ss">:delete</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="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">f</span><span class="o">.</span><span class="n">submit</span> <span class="s2">"Unfollow"</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="s2">"btn"</span> <span class="cp">%></span><span class="x"></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
</pre></div>
</div>
<p>这两个表单都使用 <code>form_for</code> 处理“关系”模型对象,二者之间主要的不同点是,<a href="#listing-follow-form">代码清单 12.21</a> 用来构建一个新“关系”,而<a href="#listing-unfollow-form">代码清单 12.22</a> 查找现有的“关系”。很显然,第一个表单会向 <code>RelationshipsController</code> 发送 <code>POST</code> 请求,创建“关系”(<code>create</code> 动作);而第二个表单发送的是 <code>DELETE</code> 请求,销毁“关系”(<code>destroy</code> 动作)。(这两个动作在 <a href="#a-working-follow-button-the-standard-way">12.2.4 节</a>编写。)你可能还注意到了,关注用户的表单中除了按钮之外什么内容也没有,但是仍然要把 <code>followed_id</code> 发送给控制器。在<a href="#listing-follow-form">代码清单 12.21</a> 中,我们使用 <code>hidden_field_tag</code> 方法把 <code>followed_id</code> 添加到表单中,生成的 HTML 如下:</p>
<div data-type="listing">
<div class="highlight language-html"><pre><span class="nt"><input</span> <span class="na">id=</span><span class="s">"followed_id"</span> <span class="na">name=</span><span class="s">"followed_id"</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">value=</span><span class="s">"3"</span> <span class="nt">/></span>
</pre></div>
</div>
<p><a href="chapter10.html#resetting-the-password">10.2.4 节</a>说过,隐藏的 <code>input</code> 标签会把所需的信息包含在表单中,但在浏览器中不会显示出来。</p>
<p>现在我们可以在资料页面中加入关注表单和数量统计了,如<a href="#listing-user-follow-form-profile-stats">代码清单 12.23</a> 所示,只需渲染相应的局部视图即可。显示有关注按钮和取消关注按钮的用户资料页面分别如<a href="#fig-profile-follow-button">图 12.12</a> 和<a href="#fig-profile-unfollow-button">图 12.13</a> 所示。</p>
<div id="listing-user-follow-form-profile-stats" data-type="listing">
<h5><span class="title-label">代码清单 12.23</span>:在用户资料页面加入关注表单和数量统计</h5>
<div class="source-file">app/views/users/show.html.erb</div>
<div class="highlight language-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="vi">@user</span><span class="o">.</span><span class="n">name</span><span class="p">)</span> <span class="cp">%></span><span class="x"></span>
<span class="x"><div class="row"></span>
<span class="x"> <aside class="col-md-4"></span>
<span class="x"> <section></span>
<span class="x"> <h1></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </h1></span>
<span class="x"> </section></span>
<span class="hll"><span class="x"> <section class="stats"></span>
</span><span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</span> <span class="cp">%></span><span class="x"></span>
</span><span class="hll"><span class="x"> </section></span>
</span><span class="x"> </aside></span>
<span class="x"> <div class="col-md-8"></span>
<span class="hll"><span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'follow_form'</span> <span class="k">if</span> <span class="n">logged_in?</span> <span class="cp">%></span><span class="x"></span>
</span><span class="x"> </span><span class="cp"><%</span> <span class="k">if</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">any?</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <h3>Microposts (</span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">count</span> <span class="cp">%></span><span class="x">)</h3></span>
<span class="x"> <ol class="microposts"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@microposts</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </ol></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="vi">@microposts</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="x"></div></span>
</pre></div>
</div>
<div id="fig-profile-follow-button" class="figure"><img src="images/chapter12/profile_follow_button_3rd_edition.png" alt="profile follow button 3rd edition" /><div class="figcaption"><span class="title-label">图 12.12</span>:某个用户的资料页面(<a href="http://localhost:3000/users/2">/users/2</a>),显示有关注按钮</div></div>
<div id="fig-profile-unfollow-button" class="figure"><img src="images/chapter12/profile_unfollow_button_3rd_edition.png" alt="profile unfollow button 3rd edition" /><div class="figcaption"><span class="title-label">图 12.13</span>:某个用户的资料页面(<a href="http://localhost:3000/users/5">/users/5</a>),显示有取消关注按钮</div></div>
<p>稍后我们会让这些按钮起作用,而且要使用两种方式实现,一种是常规方式(<a href="#a-working-follow-button-the-standard-way">12.2.4 节</a>),另一种使用 Ajax(<a href="#a-working-follow-button-with-ajax">12.2.5 节</a>)。不过在此之前,我们要创建剩下的页面——我关注的用户列表页面和关注我的用户列表页面。</p>
</section>
<section data-type="sect2" id="following-and-followers-pages">
<h2><span class="title-label">12.2.3</span> 我关注的用户列表页面和关注我的用户列表页面</h2>
<p>我关注的用户列表页面和关注我的用户列表页面是资料页面和用户列表页面混合体,在侧边栏显示用户的信息(包括数量统计),再列出一系列用户。除此之外,还会在侧边栏中显示一个用户头像列表。构思图如<a href="#fig-following-mockup">图 12.14</a>(我关注的用户)和<a href="#fig-followers-mockup">图 12.15</a>(关注我的用户)所示。</p>
<div id="fig-following-mockup" class="figure"><img src="images/chapter12/following_mockup_bootstrap.png" alt="following mockup bootstrap" /><div class="figcaption"><span class="title-label">图 12.14</span>:我关注的用户列表页面构思图</div></div>
<div id="fig-followers-mockup" class="figure"><img src="images/chapter12/followers_mockup_bootstrap.png" alt="followers mockup bootstrap" /><div class="figcaption"><span class="title-label">图 12.15</span>:关注我的用户列表页面构思图</div></div>
<p>首先,我们要让这两个页面的地址可访问。按照 Twitter 的方式,访问这两个页面都需要先登录。我们要先编写测试,参照以前的访问限制测试,写出的测试如<a href="#listing-following-followers-authorization-test">代码清单 12.24</a> 所示。</p>
<div id="listing-following-followers-authorization-test" data-type="listing">
<h5><span class="title-label">代码清单 12.24</span>:我关注的用户列表页面和关注我的用户列表页面的访问限制</h5>
<div class="source-file">test/controllers/users_controller_test.rb</div>
<div class="highlight language-ruby"><pre><span class="nb">require</span> <span class="s1">'test_helper'</span>
<span class="k">class</span> <span class="nc">UsersControllerTest</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:michael</span><span class="p">)</span>
<span class="vi">@other_user</span> <span class="o">=</span> <span class="n">users</span><span class="p">(</span><span class="ss">:archer</span><span class="p">)</span>
<span class="k">end</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="nb">test</span> <span class="s2">"should redirect following when not logged in"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:following</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="vi">@user</span>
<span class="n">assert_redirected_to</span> <span class="n">login_url</span>
<span class="k">end</span>
<span class="nb">test</span> <span class="s2">"should redirect followers when not logged in"</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:followers</span><span class="p">,</span> <span class="nb">id</span><span class="p">:</span> <span class="vi">@user</span>
<span class="n">assert_redirected_to</span> <span class="n">login_url</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
</div>
<p>在实现这两个页面的过程中,唯一很难想到的是,我们要在用户控制器中添加相应的两个动作。按照<a href="#listing-following-followers-actions-routes">代码清单 12.15</a> 中的路由设置,这两个动作应该命名为 <code>following</code> 和 <code>followers</code>。在这两个动作中,需要设置页面的标题、查找用户,获取 <code>@user.followed_users</code> 或 <code>@user.followers</code>(要分页显示),然后再渲染页面,如<a href="#listing-following-followers-actions">代码清单 12.25</a> 所示。</p>
<div id="listing-following-followers-actions" data-type="listing">
<h5><span class="title-label">代码清单 12.25</span>:<code>following</code> 和 <code>followers</code> 动作 <span class="red">RED</span></h5>
<div class="source-file">app/controllers/users_controller.rb</div>
<div class="highlight language-ruby"><pre><span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="hll"> <span class="n">before_action</span> <span class="ss">:logged_in_user</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="o">[</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="ss">:destroy</span><span class="p">,</span>
</span><span class="hll"> <span class="ss">:following</span><span class="p">,</span> <span class="ss">:followers</span><span class="o">]</span>
</span> <span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">def</span> <span class="nf">following</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="s2">"Following"</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">following</span><span class="o">.</span><span class="n">paginate</span><span class="p">(</span><span class="ss">page</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:page</span><span class="o">]</span><span class="p">)</span>
<span class="n">render</span> <span class="s1">'show_follow'</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">followers</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="s2">"Followers"</span>
<span class="vi">@user</span> <span class="o">=</span> <span class="no">User</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">followers</span><span class="o">.</span><span class="n">paginate</span><span class="p">(</span><span class="ss">page</span><span class="p">:</span> <span class="n">params</span><span class="o">[</span><span class="ss">:page</span><span class="o">]</span><span class="p">)</span>
<span class="n">render</span> <span class="s1">'show_follow'</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="o">.</span>
<span class="k">end</span>
</pre></div>
</div>
<p>读过本书前面的内容我们发现,按照 Rails 的约定,动作最后都会隐式渲染对应的视图,例如 <code>show</code> 动作最后会渲染 <code>show.html.erb</code>。而<a href="#listing-following-followers-actions">代码清单 12.25</a> 中的两个动作都显式调用了 <code>render</code> 方法,渲染一个名为 <code>show_follow</code> 的视图。下面我们就来编写这个视图。这两个动作之所以使用同一个视图,是因为两种情况用到的 ERb 代码差不多,如<a href="#listing-show-follow-view">代码清单 12.26</a> 所示。</p>
<div id="listing-show-follow-view" data-type="listing">
<h5><span class="title-label">代码清单 12.26</span>:渲染我关注的用户列表页面和关注我的用户列表页面的 <code>show_follow</code> 视图</h5>
<div class="source-file">app/views/users/show_follow.html.erb</div>
<div class="highlight language-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="vi">@title</span><span class="p">)</span> <span class="cp">%></span><span class="x"></span>
<span class="x"><div class="row"></span>
<span class="x"> <aside class="col-md-4"></span>
<span class="x"> <section class="user_info"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">gravatar_for</span> <span class="vi">@user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <h1></span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">name</span> <span class="cp">%></span><span class="x"></h1></span>
<span class="x"> <span></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"view my profile"</span><span class="p">,</span> <span class="vi">@user</span> <span class="cp">%></span><span class="x"></span></span>
<span class="x"> <span><b>Microposts:</b> </span><span class="cp"><%=</span> <span class="vi">@user</span><span class="o">.</span><span class="n">microposts</span><span class="o">.</span><span class="n">count</span> <span class="cp">%></span><span class="x"></span></span>
<span class="x"> </section></span>
<span class="x"> <section class="stats"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="s1">'shared/stats'</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">if</span> <span class="vi">@users</span><span class="o">.</span><span class="n">any?</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <div class="user_avatars"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="vi">@users</span><span class="o">.</span><span class="n">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="x"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="n">gravatar_for</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="ss">size</span><span class="p">:</span> <span class="mi">30</span><span class="p">),</span> <span class="n">user</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </section></span>
<span class="x"> </aside></span>
<span class="x"> <div class="col-md-8"></span>
<span class="x"> <h3></span><span class="cp"><%=</span> <span class="vi">@title</span> <span class="cp">%></span><span class="x"></h3></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">if</span> <span class="vi">@users</span><span class="o">.</span><span class="n">any?</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> <ul class="users follow"></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">render</span> <span class="vi">@users</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </ul></span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">will_paginate</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </span><span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span><span class="x"></span>
<span class="x"> </div></span>
<span class="x"></div></span>
</pre></div>
</div>
<p><a href="#listing-following-followers-actions">代码清单 12.25</a> 中的动作会按需渲染<a href="#listing-show-follow-view">代码清单 12.26</a> 中的视图,分别显式我关注的用户列表和关注我的用户列表,如<a href="#fig-user-following">图 12.16</a> 和<a href="#fig-user-followers">图 12.17</a> 所示。注意,上述代码都没有到“当前用户”,所以这两个链接对其他用户也可用,如<a href="#fig-different-user-followers">图 12.18</a> 所示。</p>
<div id="fig-user-following" class="figure"><img src="images/chapter12/user_following_3rd_edition.png" alt="user following 3rd edition" /><div class="figcaption"><span class="title-label">图 12.16</span>:显示某个用户关注的人</div></div>
<div id="fig-user-followers" class="figure"><img src="images/chapter12/user_followers_3rd_edition.png" alt="user followers 3rd edition" /><div class="figcaption"><span class="title-label">图 12.17</span>:显示关注某个用户的人</div></div>
<div id="fig-different-user-followers" class="figure"><img src="images/chapter12/diferent_user_followers_3rd_edition.png" alt="diferent user followers 3rd edition" /><div class="figcaption"><span class="title-label">图 12.18</span>:显示关注另一个用户的人</div></div>
<p>现在,这两个页面可以使用了,下面要编写一些简短的集成测试,确认表现正确。这些测试只是健全检查,无需面面俱到。正如 <a href="chapter5.html#layout-link-tests">5.3.4 节</a>所说的,全面的测试,例如检查 HTML 结构,并不牢靠,而且可能适得其反。对这两个页面来说,我们计划确认显示的数量正确,而且页面中有指向正确的 URL 的链接。</p>
<p>首先,和之前一样,生成一个集成测试文件:</p>
<div data-type="listing">
<div class="highlight language-sh"><pre><span class="nv">$ </span>rails generate integration_test following
invoke test_unit
create <span class="nb">test</span>/integration/following_test.rb
</pre></div>
</div>
<p>然后,准备测试数据。我们要在“关系”固件中创建一些关注关系。<a href="chapter11.html#profile-micropost-tests">11.2.3 节</a>使用下面的代码把微博和用户关联起来:</p>
<div data-type="listing">
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain">orange</span><span class="p-Indicator">:</span>
<span class="l-Scalar-Plain">content</span><span class="p-Indicator">:</span> <span class="s">"I</span><span class="nv"> </span><span class="s">just</span><span class="nv"> </span><span class="s">ate</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">orange!"</span>
<span class="l-Scalar-Plain">created_at</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain"><%= 10.minutes.ago %></span>
<span class="l-Scalar-Plain">user</span><span class="p-Indicator">:</span> <span class="l-Scalar-Plain">michael</span>
</pre></div>
</div>
<p>注意,我们没有用 <code>user_id: 1</code>,而是 <code>user: michael</code>。</p>
<p>按照这样的方式编写“关系”固件,如<a href="#listing-relationships-fixtures">代码清单 12.27</a> 所示。</p>
<div id="listing-relationships-fixtures" data-type="listing">
<h5><span class="title-label">代码清单 12.27</span>:“关系”固件</h5>
<div class="source-file">test/fixtures/relationships.yml</div>
<div class="highlight language-yaml"><pre><span class="l-Scalar-Plain">one</span><span class="p-Indicator">:</span>