PageRenderTime 24ms CodeModel.GetById 18ms app.highlight 3ms RepoModel.GetById 0ms app.codeStats 0ms

/episodes/163/en.html

https://github.com/mrnbrkt/asciicasts.com-translations
HTML | 194 lines | 159 code | 35 blank | 0 comment | 0 complexity | 055a88c834e9492a4715eab1b16702cc MD5 | raw file
  1<p>Below is a page from a basic social networking application. A user can sign up, then log in and interact with other users. The page shown displays a list of the users and has a link next to each one that allows you to add them as a friend.</p>
  2<div class="imageWrapper">
  3  <img src="/system/photos/110/original/E163I01.png" width="808" height="452" alt="The users list page on our application."/>
  4</div>
  5
  6<p>Although the link is there, it currently does nothing. In this episode we&rsquo;ll write the code needed to enable a user to add other users as friends. To do this we&rsquo;re going to need to create a self-referencing association: users will have relationships with other users, but instead of creating a relationship between two different models, we&rsquo;ll be creating a relationship between two instances of the same model.</p>
  7
  8<h3>Generating The Right Actions</h3>
  9
 10<p>The &ldquo;Add Friend&rdquo; link on the users page doesn&rsquo;t as yet link to anything.</p>
 11<pre class="ruby">
 12&lt;%= link_to &quot;Add Friend&quot; %&gt;
 13</pre>
 14<p>At some point we&rsquo;re going to have to wire it up to an action in a controller, and it&rsquo;s worth thinking a little now about what controller and action it will call when it&rsquo;s clicked. We already have a <code>UsersController</code>, so this might seem the obvious place to deal with friends as friends are just other users. We could add two new actions to the <code>UsersController</code> to deal with adding and removing friends.</p>
 15<pre class="ruby">
 16def add_friend
 17    
 18end
 19  
 20def remove_friend
 21    
 22end
 23</pre>
 24<p>This, however, is wrong for several reasons. The first alarm bell that should ring in your head when you start creating controller methods like these is that they are not among the seven standard RESTful methods (index, show, new, create, edit, update and destroy). Another concern should be raised by the fact that these methods both have <code>_friend</code> appended to them. This suggests that we&rsquo;re putting these methods in their own namespace. The final clue that this isn&rsquo;t the best approach is the first part of each method name: <code>add</code> and <code>remove</code> indicate that we&rsquo;re creating and destroying a resource. All of these points give us a big hint that we should be creating a new controller for handling friends.</p>
 25
 26<h3>Creating Friendships</h3>
 27
 28<p>The controller we&rsquo;ll create will be called <code>FriendshipsController</code>. The naming of this controller is important. We might have called it <code>FriendsController</code> but in our application a friend is just another <code>User</code>, and we&rsquo;ll be using our new controller to create and destroy relationships between users, not to create and destroy users.</p>
 29
 30<p>A user can have many friends and be befriended by many other users so we&rsquo;re going to have to create a many-to-many relationship. There are two ways to define this type of relationship in Rails: <code>has_and_belongs_to_many</code> and <code>has_many :through</code>. (You can find more information about these two methods by watching <a href="http://railscasts.com/episodes/47-two-many-to-many">Railscast 47</a>). These days, <code>has_many :through</code> is the most used, and it&rsquo;s what we&rsquo;ll be using to define our friendships.</p>
 31
 32<p>To use <code>has_many :through</code> we&rsquo;ll need to create a join model, which we&rsquo;ll call <code>Friendship</code>. This model will have two fields, a <code>user_id</code> that represents the current user who&rsquo;s adding a friend and a <code>friend_id</code> that represents the user who&rsquo;s being befriended.</p>
 33
 34<p>We&rsquo;ll create our new model in the usual way,</p>
 35<pre class="terminal">
 36script/generate model Friendship user_id:integer friend_id:integer
 37</pre>
 38<p>and then run the generated migration.</p>
 39<pre class="terminal">
 40rake db:migrate
 41</pre>
 42<p>Along with the model we&rsquo;ll need to generate the <code>FriendshipsController</code> we mentioned earlier.</p>
 43<pre class="terminal">
 44script/generate controller Friendships
 45</pre>
 46<p>As we&rsquo;re using <code>Friendship</code> as a resource we&rsquo;ll need to add the following line to <code>/config/routes.rb</code>.</p>
 47<pre class="ruby">
 48map.resources :friendships
 49</pre>
 50<p>Now that we have our model and controller we can start to define how a <code>Friendship</code> works. We&rsquo;ll start off by defining the relationships between <code>User</code> and <code>Friendship</code> in the model classes.</p>
 51<pre class="ruby">
 52class Friendship &lt; ActiveRecord::Base
 53  belongs_to :user
 54  belongs_to :friend, :class_name =&gt; &#x27;User&#x27;
 55end
 56</pre>
 57<p>A <code>Friendship</code> will <code>belong_to</code> a <code>User</code>, who will be the user who initialised the relationship, and will also belong to a <code>Friend</code>, who is the user who has been befriended. For the <code>friend</code> relationship we need to explicitly specify the class name as ActiveRecord cannot work out the model to use from the name in the association.</p>
 58
 59<p>We&rsquo;ll also need to define the other half of the relationship in the <code>User</code> model.</p>
 60<pre class="ruby">
 61class User &lt; ActiveRecord::Base
 62  
 63  has_many :friendships
 64  has_many :friends, :through =&gt; :friendships
 65
 66  # rest of class omitted. 
 67end
 68</pre>
 69<p>The relationship here is defined as you would define any other many-to-many relationship. There&rsquo;s no need to specify any custom naming as the relationships can all be worked out from the names of the relationships.</p>
 70
 71<h3>Wiring Up The &ldquo;Add Friend&rdquo; Link</h3>
 72
 73<p>Now that the relationships between the models have been defined, and we have our <code>FriendshipsController</code> we can start work on the &ldquo;Add Friend&rdquo; link in the users&rsquo; <code>index</code> view.</p>
 74<pre class="ruby">
 75&lt;% for user in @users %&gt;
 76  &lt;div class=&quot;user&quot;&gt;
 77    &lt;p&gt;
 78      &lt;strong&gt;&lt;%=h user.username %&gt;&lt;/strong&gt;
 79      &lt;%= link_to &quot;Add Friend&quot;, friendships_path(:friend_id =&gt; user), :method =&gt; :post %&gt;
 80      &lt;div class=&quot;clear&quot;&gt;&lt;/div&gt;
 81    &lt;/p&gt;
 82  &lt;/div&gt;
 83&lt;% end %&gt;
 84</pre>
 85<p>The link needs to call the <code>create</code> method in the <code>FriendshipsController</code>. To do that we have the link POST to the <code>friendships_path</code>. That isn&rsquo;t quite enough though; we also need to pass the user that we&rsquo;re going to befriend. We do that by adding it as a parameter to the <code>friendships_path</code> method. That will pass the <code>id</code> of the user to the <code>create</code> action so that it knows which user we&rsquo;re befriending.</p>
 86
 87<p>Now that the link code is written we&rsquo;ll move on to the <code>FriendshipsController</code> and write the <code>create</code> action.</p>
 88<pre class="ruby">
 89def create
 90  @friendship = current_user.friendships.build(:friend_id =&gt; params[:friend_id])
 91  if @friendship.save
 92    flash[:notice] = &quot;Added friend.&quot;
 93    redirect_to root_url
 94  else
 95    flash[:notice] = &quot;Unable to add friend.&quot;
 96    redirect_to root_url
 97  end
 98end
 99</pre>
100<p>We create the new friendship by taking our current user and building a friendship through it, passing it the <code>id</code> of the user we&rsquo;re befriending. Using <code>build</code> will mean that the <code>user_id</code> of the new <code>Friendship</code> is automatically set, and as we&rsquo;ve passed the <code>friend_id</code> from the parameters the relationship is fully defined. We can then save the relationship and redirect back to the home page, displaying a <code>flash</code> notice that says that the friend has been added. If for some reason the relationship is invalid then we&rsquo;ll show a different <code>flash</code> notice. Obviously, if this was an application that was going into production then we&rsquo;d give the user more specific information as to why their request failed.</p>
101
102<h3>Viewing Our Friends</h3>
103
104<p>A logged-in user can now click the &ldquo;Add Friend&rdquo; link and make another user their friend. They can&rsquo;t, however, see a list of their friends. To fix this we&rsquo;ll modify the user&rsquo;s profile page so that they can see who they have added as a friend. The profile page is the <code>show</code> page for a <code>User</code>. Currently it just shows their name and provides a link that allows them to find friends.</p>
105<pre class="ruby">
106&lt;% title &quot;My Profile&quot; %&gt;
107&lt;p&gt;Username: &lt;%=h @user.username %&gt;&lt;/p&gt;
108&lt;p&gt;&lt;%= link_to &quot;Find Friends&quot;, users_path %&gt;&lt;/p&gt;
109</pre>
110<p>To display a list of their friends, we can loop through the user&rsquo;s <code>friends</code> collection and display each one.</p>
111<pre class="ruby">
112&lt;h2&gt;Your Friends&lt;/h2&gt;
113&lt;ul&gt;
114  &lt;% for user in @user.friends %&gt;
115  &lt;li&gt;&lt;%= h. user.username %&gt;&lt;/li&gt;
116  &lt;% end %&gt;  
117&lt;/ul&gt;
118</pre>
119<p>The profile page will now show the list of our friends.</p>
120
121<div class="imageWrapper">
122  <img src="/system/photos/111/original/E163I02.png" width="808" height="452" alt="Our friends are now listed on our profile page."/>
123</div>
124
125<h3>Removing Friends</h3>
126
127<p>It would be useful if there was a link next to each of our listed friends that allowed us to remove that user as a friend. To implement that we need to add a link to the <code>FriendshipController</code>&rsquo;s <code>destroy</code> action, passing the <code>id</code> of the friendship. It&rsquo;s here where things could get a little tricky as we&rsquo;re looping through <code>User</code>s and don&rsquo;t have access to the <code>Friendship</code> model to get the <code>id</code> that identifies the relationship. This is a common problem that beginners to Rails encounter when they use many-to-many relationships as they can forget that there is a join model that we can interact with directly rather than, in this case jumping directly from <code>User</code> to <code>Friend</code>.</p>
128
129<p>Instead of looping through a user&rsquo;s friends to create the list, we&rsquo;ll instead loop through their friendships. The code in the view will now change to this:</p>
130<pre class="ruby">
131&lt;h2&gt;Your Friends&lt;/h2&gt;
132&lt;ul&gt;
133  &lt;% for friendship in @user.friendships %&gt;
134  &lt;li&gt;
135    &lt;%= h friendship.friend.username %&gt;
136    (&lt;%= link_to &quot;remove&quot;, friendship, :method =&gt; :delete %&gt;)
137  &lt;/li&gt;
138  &lt;% end %&gt;  
139&lt;/ul&gt;
140</pre>
141<p>We can now get each friend&rsquo;s name by using <code>friendship.friend.username</code> and create our delete link by passing the friendship as a parameter to the link and telling it to use DELETE as the method so that it will call the <code>destroy</code> action.</p>
142
143<p>Talking of the <code>destroy</code> action, it&rsquo;s now time to implement it in the <code>FriendshipsController</code>.</p>
144<pre class="ruby">
145def destroy
146  @friendship = current_user.friendships.find(params[:id])
147  @friendship.destroy
148  flash[:notice] = &quot;Removed friendship.&quot;
149  redirect_to current_user
150end
151</pre>
152<p>Note that in the first line of the action we search for the friendship only within the current user&rsquo;s friendships. If we just called <code>Friendship.find(params[:id])</code> then a maliciously minded user could destroy relationships between any two users, when they should be restricted to only destroying relationships they themselves have made. The rest of the action destroys the friendship and then redirects back to the user&rsquo;s profile page.</p>
153
154<p>The profile page will now show a link and we&rsquo;ll be able to remove a friendship with another user by clicking the link next to their name.</p>
155
156<div class="imageWrapper">
157  <img src="/system/photos/112/original/E163I03.png" width="808" height="452" alt="Removing a friend from our list."/>
158</div>
159
160<h3>Inverse Relationships</h3>
161
162<p>When creating self-referential relationships it&rsquo;s important to remember that we&rsquo;re only creating one side of the relationship. Although <code>eifion</code> has <code>paul</code> listed as a friend above, if we were to visit <code>paul</code>&rsquo;s profile we wouldn&rsquo;t see <code>eifion</code> listed unless <code>paul</code> had chosen to add him. We need two <code>Friendship</code> records to create a mutual friendship.</p>
163
164<p>To finish this episode we&rsquo;ll add a list to the profile page so that a user can see who has them listed as a friend so that we can see the other side of this relationship. To do this we need to add two more relationships to the <code>User</code> model.</p>
165<pre class="ruby">
166class User &lt; ActiveRecord::Base
167  
168  has_many :friendships
169  has_many :friends, :through =&gt; :friendships
170  
171  has_many :inverse_friendships, :class_name =&gt; &quot;Friendship&quot;, :foreign_key =&gt; &quot;friend_id&quot;
172  has_many :inverse_friends, :through =&gt; :inverse_friendships, :source =&gt; :user
173
174  #rest of class omitted.
175end
176</pre>
177<p>It&rsquo;s difficult to think up appropriate names to define the other side of the relationship so we&rsquo;ll prefix both with the word &ldquo;inverse&rdquo; to give us <code>inverse_friendships</code> and <code>inverse_friends</code>. We need to specify some additional options to make the relationships work. For <code>inverse_friendships</code> we&rsquo;ll have to specify the name of the other model as it can&rsquo;t be inferred from the relationship name and we&rsquo;ll also have to define the foreign key as <code>friend_id</code>. For the <code>inverse_friends</code> relationship we need to specify the <code>source</code> as <code>users</code>, as again it cannot be inferred from the name of the relationship.</p>
178
179<p>Back in the profile page we want to show a list of the users who have befriended us. To do this we just need to loop through our <code>inverse_friends</code> and display their user names.</p>
180<pre class="ruby">
181&lt;h2&gt;Users Who Have Befriended You&lt;/h2&gt;
182&lt;ul&gt;
183  &lt;% for user in @user.inverse_friends %&gt;
184  &lt;li&gt;&lt;%= h user.username %&gt;&lt;/li&gt;
185  &lt;% end %&gt;
186&lt;/ul&gt;
187</pre>
188<p>If we log in as <code>paul</code> and add <code>eifion</code> as one of his friends, then log back in as <code>eifion</code> we&rsquo;ll now see <code>paul</code> listed as having added <code>eifion</code> as a friend.</p>
189
190<div class="imageWrapper">
191  <img src="/system/photos/113/original/E163I04.png" width="808" height="472" alt="We can now see who call us a friend."/>
192</div>
193
194<p>That&rsquo;s it for this episode. We haven&rsquo;t quite recreated Facebook, but hopefully you&rsquo;ve now got a good idea of how self-referential relationships work in Ruby on Rails.</p>