Relationships between Users

Por:, en:

Wouldn't it be nice to give our users the ability to become friends on our platform? They could friend each other and be notified about their friend's activity.

Great. So how do we go about that?

Referencing models

You may think that the simple way to do this would be to add a friend_id column to the user table in the database and save IDs of friends there. The problem with this solution is that every field with our relational database is only accepting one value. What if there is a situation that we want to be friends with all users on our website?

It looks like we have to create a completely different way of keeping track of our relationships. We will create a new model to store information about relationships inside of it. We will call this model a "relationship". It will keep both ids of both people establishing relationship. One person will be "inviting" and the other person will be "invited".

Technically we could make our relationship more flexible so that it doesn't care about who is inviting and who is invited, but that, contrary to common sense would actually make it more complex.

Referencing relationships and users by their IDs will make it very easy to count how many connections people have, who they are, who are their friends without duplicating any information.

Also, we can easily delete relationship without affecting user record. For disclosure: this is not the only way of doing this, however SQL rows store database we're using is optimised for this kind or relationships. We will be able to find all friends of a user very very fast.

Now. let's go ahead and create model.

Relationship Model

bash


$ rails generate model Relationship inviting_id:integer invited_id:integer

    invoke  active_record
    create    db/migrate/20140526071934_create_relationships.rb
    create    app/models/relationship.rb
    invoke    test_unit
    create      test/models/relationship_test.rb
    create      test/fixtures/relationships.yml

db/migrate/20140526071934_create_relationships.rb


class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :inviting_id
      t.integer :invited_id

      t.timestamps
    end
  end
end

We can now migrate the database:

bash


$ rake db:migrate

Excellent, we can now create relationships. Let's go ahead and create one. If you created only one user in your application, first create another one. To do that you can simply logout from your website and register with different email. Now open terminal and start rails console:

bash


$ rails console

console


> user_one = User.first

> user_two = User.last

console


> relationship = Relationship.create(inviting_id: user_one.id, invited_id: user_two.id)


    (0.2ms)  begin transaction
    SQL (0.6ms)  INSERT INTO "relationships" ("created_at", "invited_id", "inviting_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2014-05-26 07:30:08.019316"], ["invited_id", 2], ["inviting_id", 1], ["updated_at", "2014-05-26 07:30:08.019316"]]
    (0.6ms)  commit transaction
    => #<Relationship id: 1, inviting_id: 1, invited_id: 2, created_at: "2014-05-26 07:30:08", updated_at: "2014-05-26 07:30:08">

We have now created a relationship. However it's still quite hard to use. Let's try to count how many invitations has user_one sent:

console


> Relationship.where(inviting_id: user_one.id).count


    > Relationship.where(inviting_id: user_one.id).count
    (0.2ms)  SELECT COUNT(*) FROM "relationships"  WHERE "relationships"."inviting_id" = 1
    => 1

Sure, we can do that, but it's not a nice code. Thankfully, Rails has better way of finding the relationships between records in the database. We have to add some code to the user model:

app/models/user.rb

#existing code

has_many :posts, dependent: :destroy
has_many :sent_invites, class_name: "Relationship", foreign_key: :inviting_id
has_many :received_invites, class_name: "Relationship", foreign_key: :invited_id

#existing code

This code allows us to match user and relationships. We have previously used "has_many" and "belongs_to" but back then we had used two different entities - for example users and posts. This time, we have same type of entity entering relationship. User - User. Because we need differentiate to who is inviting and who is invited therefore we need to rename the "owners" of the relationship.

Instead of has_many :relationships in the user.rb we say has_many :sent_invites. The problem that this brings is that rails can not match anything by name, which normally happens automatically. Therefore we need to be more specific. We declare that "sent_invite" is in fact instance of a class Relationship.

Both sent_invites and received_invites are both referring to Relationship. The only difference between them is that one will use inviting_id and the other invited_id. Have a look at this chart, it should make things a little clearer:

Forming relationship between users:

forming relationship between users


Finding invites sent BY user

finding invites sent to user


Finding invites sent TO user

finding invitations sent by user


By adding those two lines of code we're building the association between users and relationships that hold record of who is who's friend.

After we add those two lines we can now write following code (make sure you restart your console):

rails console


> user = User.first
> user.sent_invites.count
    => 1

> user.received_invites
    => 0

Now, instead of defining both inviting user ID an well invited user ID when creating relationship, we can simply write code that creates relationship "through" inviting user:

rails console


> user_one = User.first
> user_two = User.last

> relationship = user_one.sent_invites.create(invited_id: user_two.id)

We also want to be able to ask for the user who invited or has been invited when we have the relationship. We would like to write "relationship.inviting_user" or "relationship.invited_user". To be able to do that, we need to "tell" our relationship, who are invited and inviting users:

app/models/relationship.rb


class Relationship < ActiveRecord::Base

    belongs_to :invited_user, class_name: 'User', foreign_key: :invited_id
    belongs_to :inviting_user, class_name: 'User', foreign_key: :inviting_id

end

After making this change, restart your console and try writing following query:

rails console


> relationship = Relationship.last

> relationship.inviting_user.email
    => emails_of_the_user@example.com

This is quite powerful. We will leverage ability to search for connected objects. We will eventually be able to find posts by all users we have relationship with, send emails with activity updates etc.

For now however let's add ability for find invited users and those who send invitations through the "Relationship" model. We will have to add some code inside user.rb

app/models/user.rb


    has_many :sent_invites, class_name: 'Relationship', foreign_key: :inviting_id
    has_many :received_invites, class_name: 'Relationship', foreign_key: :invited_id

    has_many :invited_users, through: :sent_invites, source: :invited_user
    has_many :inviting_users, through: :received_invites, source: :inviting_user

After you restart your console you will be able to write following code:

ruby console


> user = User.first
> user.invited_users.count
    => 1
> user.invited_users.first.email
    => "the_email_you_have_used@example.com"

> user.inviting_users.count
    => 0

Creating Relationships

Great, we can create relationships from the command line but what about user interface? We will create special "button" that will allow us to trigger relationship creation process.

We need to begin by creating new controller. We will call it "relationships". This controller will be responsible for creating and destroying our relationships. And we will make our button go directly to actions inside this controller. Let's go ahead and generate this controller:

bash


$ rails generate controller relationships

We can now add new action to the controller. When designing our REST API we use 7(in some cases 8) actions:

action name what we use it for do we create HTML template?
New Display form to create new record Yes
Create Save the record submitted by form from "new" template and redirect No, we redirect
Edit Display the form for editing existing record Yes
Update Save the changes submitted by form from "edit" template and redirect No, we redirect
Destroy Delete record and redirect
Show Individual page showing the record (ex. user profile, or blog entry) Yes
Index List of records Yes

Of course we can completely override this structure. We can create custom action and call it "vanilla_ice_cream" and make it delete all records in the database. But why would we do it? Great thing about following the standard is that if everybody can collaborate on projects without necessity for introduction to the project. Everyone knows how your app works, and you will know others' apps structure too.

Now, let's go back to the list of actions we can use and see which ones can be applied to our case of creating relationships.

We want our "follow" button to be located on the user's profile.

  • New - No. We don't need it for relationships. We will not be building dedicated page for creating relationships. Instead, we will put button to create relationship on user's profile page (users controller, show action)
  • Create - Yes. We will define create action to save relationship between users and redirect after success.
  • Edit - No. We don't need to edit relationships. They are quite binary - you're either in relationship or not (although we all have friends who would disagree).
  • Update - No. Since we won't be submitting changes, we won't need to save anything.
  • Destroy - Yes. When users decide that they want to "unfriend" someone. We will destroy that relationship.
  • Show - No. We, don't need to have whole page dedicated to the relationship. Of course you may want to have change that behavior.
  • Index - Yes. We will be listing user's relationships.

That simplifies things, we will not be using all the actions. We can pick and choose those, that make sense in our case.

First, let's make sure we have routes that can map to the actions we're going to work with.

config/routes.rb


Rails.application.routes.draw do
    #existing code
    resources :relationships, only: [:create, :destroy, :index]
end

Now we can proceed with defining our controller actions one by one:

app/controllers/relationships_controller.rb


class RelationshipsController < ApplicationController
  def create
    render text: params
  end
end

We defined create action but for now we don't need it to perform any advanced work. We will simply render all parameters submitted to this action by our form-button (which we're yet to build). We render those parameters as simple text. Now, we need to create form that will be able to communicate with this action. We will do it inside user's show page. Copy the code from line 7 to line 10:

app/views/users/show.html.erb


<div class="row profile">
    <div class="col-sm-4 col-md-3">
        <%= image_tag(@user.avatar.url(:medium), class: 'avatar') %>
        <h1><%= @user.full_name %></h1>

        <%= form_for :relationship, url: relationships_path, html: { method: :post } do |f| %>
            <%= f.hidden_field :invited_id, value: @user.id %>
            <%= f.submit 'Invite', class: 'btn btn-primary' %>
        <% end %>

        <ul>
            <%= content_tag(:li, ("Name: " + @user.name)) unless @user.name.blank? %>
            <%= content_tag(:li, ("Age: " + distance_of_time_in_words(Time.now, @user.date_of_birth) + " old")) unless @user.date_of_birth.blank? %>
        </ul>
    </div>
    <div class="feed">
        <h3>Social Feed Coming</h3>
        <p>Put a couple of lines of text here and see how it scrolls</p>
    </div>
</div>

As you can see, our form contains only two elements: hidden field with the id of a user to whom this profile belongs and a button to submit. Because id field will be auto populated, whole form looks like a single button, which is experience we want to offer to our user. Now, go to http://0.0.0.0:3000/users/  and "invite" an user.

listing parameters submitted by form

We know, that the form submits the data to the right controller action. Don't worry about the syntax of the form too much. We will dive into details in future tutorials.

We can now finish our create action so that it does what is supposed to:

app/controllers/relationships_controller.rb


class RelationshipsController < ApplicationController
  def create
    @invited_user = User.find(params[:relationship][:invited_id])

    @relationship = current_user.sent_invites.build(invited_id: @invited_user.id)

    if @relationship.save
        flash[:success] = "Successfully invited"
        redirect_to @invited_user
    else
        flash[:danger] = "Unsuccessful"
        redirect_to
    end
  end
end

Now that we can create the relationship, we also want to be able to change button that says "Invite" to a button that says "Stop following" if user if already in relationship with another user. For disclosure, there is more than one way to do that:

app/views/user/show.html.erb



<% @relationship = current_user.sent_invites.where(invited_id: @user.id).first %>

<% if @relationship %>
  LINK TO DESTROY RELATIONSHIP
<% else %>
  <%= form_for :relationship, url: relationships_path, html: { method: :post } do |f| %>
    <%= f.hidden_field :invited_id, value: @user.id %>
    <%= f.submit 'Follow', class: 'btn btn-primary' %>
  <% end %>
<% end %>

Let's look at the above code. We start with a query: <% @relationship = current_user.sent_invites.where(invited_id: @user.id).first %>. This code checks in the database if there are any sent_invites (relationships) where invited_id is the same as the id of the user to which this profile belongs.

Writing this in a following way is considered not a good practice but right now it will make it easier to understand what is going on here.

Later we have if/else statement that checks if invitation has been sent and if so, to hide the "Invite" button and show link to stop following.

Now we can do the destroy action to remove our invitation.

app/controllers/relationships_controller.rb


def destroy
    @relationship = Relationship.find(params[:id])
    @relationship.destroy
    flash[:success] = "Removed relationship"
    redirect_to @relationship.invited_user
end

And the link code:

app/views/user/show.html.erb


<% @relationship = current_user.sent_invites.where(invited_id: @user.id).first %>

<% if @relationship %>
    <%= link_to "Stop following", @relationship, method: :delete %>
<% else %>
    <%= form_for :relationship, url: relationships_path, html: { method: :post } do |f| %>
        <%= f.hidden_field :invited_id, value: @user.id %>
        <%= f.submit 'Follow', class: 'btn btn-primary' %>
    <% end %>
<% end %>

Still, there is couple of issues with this code:

  • Everyone, even not logged in users, can see the buttons
  • Everyone, even not logged in, can theoretically interact with the create and delete actions
  • Everyone, can delete relationship, even if they don't own it

Let's fix those problems one by one.

Hiding button for not logged in users

This one is easy:

app/views/user/show.html.erb


<% if current_user %>
  <% @relationship = current_user.sent_invites.where(invited_id: @user.id).first %>

  <% if @relationship %>
    <%= link_to "Stop following", @relationship, method: :delete %>
  <% else %>
    <%= form_for :relationship, url: relationships_path, html: { method: :post } do |f| %>
      <%= f.hidden_field :invited_id, value: @user.id %>
      <%= f.submit 'Follow', class: 'btn btn-primary' %>
    <% end %>
  <% end %>
<% end %>

That fixes issue with people who are not logged in being able to see the button. We use if current_user that checks if user, who is browsing the page, is logged in.

Restricting access

Because we're using Devise, we can restrict the access for all users that are not logged in by adding before_filter

app/controllers/relationships_controller.rb


class RelationshipsController < ApplicationController

   before_filter :authenticate_user!

   #exisitng code
end

This will allow only logged in user to interact in any way with the actions.

Allowing only owners to delete their own invitations

Again, this is fairly straightforward. We need to modify our destroy action.

app/controllers/relationships_controller.rb


def destroy
  @relationship = Relationship.find(params[:id])
  if @relationship.inviting_user == current_user
    @relationship.destroy
    flash[:success] = "Removed relationship"
  end
  redirect_to @relationship.invited_user
end

List of followers

Final thing for this tutorial is to list our relationships. in order to do that we need a dedicated page. "Index" sounds like something we might be looking for. Let's go ahead and add new action to our controller:

app/controllers/relationships_controller.rb


#existing code

def index
end

And create matching html template index.html.erb and put it inside relationships folder in views

app/views/relationships/index.html.erb


<h1>Connected users</h1>

Now, inside of the controller we need to find all relationships, where we're inviting and relationships, where someone invited us back. Later we will modify this behavior to be more like Facebook or LinkedIn. For now, it will look like Twitter.

app/controllers/relationships_controller.rb


def index
    @sent_invites = current_user.sent_invites
    @received_invites = current_user.received_invites
end

app/views/relationships/index.html.erb


<% if @sent_invites.any? %>
    <h3>Invited user</h3>
    <%= paginate(@sent_invites) %>
    <ul>
      <% @sent_invites.each do |invitation| %>
          <li><%= link_to invitation.invited_user.full_name, user_path(invitation.invited_user) %></li>
      <% end %>
    </ul>
    <%= paginate(@sent_invites) %>
<% end %>

<% if @received_invites.any? %>
    <h3>Invitations received</h3>
    <%= paginate(@received_invites) %>
    <ul>
      <% @received_invites.each do |invitation| %>
          <li><%= link_to invitation.invited_user.full_name, user_path(invitation.invited_user) %></li>
      <% end %>
    </ul>
    <%= paginate(@received_invites) %>
<% end %>

Let's make sure, the link to the right page is the menu of our website:

app/views/layouts/_menu.html.erb


<div class="collapse navbar-collapse" id="navbar-collapse-1">
    <ul class="nav navbar-nav navbar-right">
        <li><%= link_to 'Members', users_path %></li>
        <li><%= link_to 'Posts', posts_path %></li>
        <% if current_user %>
            <li><%= link_to 'Edit Profile',edit_user_registration_path %></li>

            <li><%= link_to 'Relationships', relationships_path %></li>

            <li><%= link_to 'Logout', destroy_user_session_path, method: :delete %></li>
            <li class="round-image-50"><%= image_tag(current_user.avatar.url(:thumb)) %></li>
        <% else %>
            <li><%= link_to 'Login', new_user_session_path %></li>
        <% end %>
    </ul>
</div>

Because this page will only be showing relationships related to current_user we don't need to hide relationships of certain kind. In future tutorials we will work on the flow and process of "accepting the invitation", sending emails with notifications and looking at friends of each user.

Si tu Inicio de sesión serás capaz de marcar esto tutorial Como vas avanzando tu progreso



Comentar

Tú puedes Inicio de sesión Comentar