Relacje między Użytkownikami

Przez:, Z dnia:

Czy nie byłoby fajnie dać naszym użytkownikom możliwość zostawania przyjaciółmi na naszej stronie? Mogliby się przyjaźnić i byliby powiadamiani o aktywności przyjaciół.

Wspaniale. Więc od czego zaczynamy?

Modele referencyjne

Możesz myśleć, że najłatwiej byłoby dodać kolumnę friend_id do tablicy użytkownika w bazie danych i zapisywać w niej identyfikatory przyjaciół. Problem z tym rozwiązaniem jest taki, że każde pole z naszą pokrewną bazą danych może przyjąć tylko jedną wartość. Co jeśli zechcemy być przyjaciółmi ze wszystkimi użytkownikami na stronie?

Wygląda na to, że musimy stworzyć zupełnie inną metodę na przechwytywanie naszych relacji. Stworzymy nowy model do przechowywania informacji o relacjach. Nazwiemy go "relationship". Będzie on przechowywał identyfikatory obydwu osób tworzących relację. Jedną z nich będzie "inviting" ("zapraszający"), a drugą będzie "invited" ("zaproszony").

Technicznie możemy sprawić, że nasze relacje będą bardziej elastyczne - tak, że nie będzie potrzebna wiadomość kto jest zapraszającym, a kto zaproszonym; ale wtedy, przciwnie zdrowemu rozsądkowi, utrudnimy sobie pracę.

Relacje referencyjne i uzytkownicy z ich identyfikatorami umożliwią łatwe zliczanie wszystkich powiązań, dowiadywanie się kim są i kim są ich przyjaciele, bez zduplikowania informacji.

Ponadto, możemy łatwo usunąć relację bez wpływania na rekord użytkownika. Dla ujawnienia: nie jest to jedynym sposobem na zrobienie tego, jednakże przchowywanie wirszy SQL bazy danych, której używamy, jest optymalizowana dla relacji takiego rodzaju.

Zatem. Zacznijmy działać i stwórzmy model.

Model "Relationship"

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

Możemy teraz zmigrować bazę danych:

bash

$ rake db:migrate

Doskonale, możemy teraz tworzyć relacje. Śmiało, stwórzmy jedną. Jeżeli stworzyłeś tylko jednego użytkownika w Twojej aplikacji, na początek stwórz jeszcze jednego. By tego dokonać możesz zwyczajnie się wylogować i zarejestrować nowego z innym emailem. Teraz otwórz terminal i odpal konsolę rails:

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">

Właśnie stowrzyliśmy relację. Jednakże, wciąż trudno jej używać. Spróbujmy wyliczyć ile zaproszeń wysłał user_one:

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

Oczywiście możemy tego dokonać, ale nie jest to ładny kod. Szczęśliwie, Rail ma lepszy sposób na znajdowanie relacji pomiędzy rekordami w bazie danych. Musimy dodać trochę kodu do modelu użytkownika:

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

Ten kod pozwala nam dopasować użytkownika i relacje. Poprzednio używaliśmy "has_many" i "belongs_to", ale wtedy używalismy dwóch innych istot - na przykład użytkownicy i posty. Tym razem mamy ten sam typ istoty zawierającej relację. Użytkownik - Użytkownik. Ponieważ potrzebyjemy rozróżnić kto jest zapraszającym i kto jest zaproszonym musimy zmienić nazwę "posiadaczy" relacji.

Zamiast has_many :relationships w user.rb napiszemy has_many :sent_invites. Problem jest taki, że Rails nie potrafi dopasować niczego po jego nazwie, co dzieje się automatycznie. Dlatego też, musimy byc bardziej precyzyjni. Deklarujemy, że "sent_invite" tak naprawdę jest instancję klasy Relationship.

Zarówno sent_invites i received_invites odwołują się do Relationship. Jeyną różnicą między nimi jest to, że jeden będzie używał inviting_id, a drugi invited_id. Spójrz na mapę poniżek, powinna rozjaśnić niektóre rzeczy:

Formowanie relacji między użytkownikami:

forming relationship between users


Znajdowanie zaproszeń wysłanych PRZEZ użytkownika:

finding invites sent to user


Znajdowanie zaproszeń wysłanych DO użytkownika:

finding invitations sent by user


Przez dodanie tych dwóch linijek budujemy powiązanie między uzytkownikami i relacjami, które trzymają rekordy kto jest czyim przyjacielem.

Po tym jak dodaliśmy te dewie linijki możemy napisać (upewnij się, że zrestartowałeś konsolę):

rails console

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

Teraz, zamiast definiowania identyfikatorów użytkowników zapraszających i zaproszonych podczas tworzenia relacji, możemy po prostu napisać kod, który tworzy relację "przez" ("through") użytkownika zapraszającego:

rails console

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

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

Chcemy także wiedzieć kto zaprasza i kto jest zaproszony. Chcielibyśmy móc napisać coś takiego "relationship.inviting_user" albo "relationship.invited_user". Aby mieć taką możliwość, musimy "powiedzieć" naszym Relacjom, kto jest użytkownikiem zapraszającym, a kto zaproszonym:

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

Po dokonaniu tych zmian, zrestartuj konsolę i spróbuj napiszać poniższy kod:

rails console

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

Jest to całkiem potężne. Wykorzystaliśmy możlwość wyszukiwania połączonych obiektów. Ostatecznie będziemy zdolni do wyszukiwania postów stworzonych przez użytkowników, z którymi tworzymy relację, wysyłania emailów z aktualizacjami aktywności, itd.

Jednak na chwilę obecną, dodajmy możliwość znajdowania zaproszonych i zapraszających użytkowników przez model "Relationship". Musimy dodać trochę kodu do 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

Po zrestartowaniu konsoli będziesz mógł napisać poniższy kod:

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

Tworzenie Relacji

Wspaniale, możemy tworzyć relację z wiersza polecenia, ale jak z iterfejsu użytkownika? Stworzymy specjalny "przycisk", który pozwoli na wywołanie procesu tworzenia relacji.

Zczniemy od stowrzenia nowego kontrolera. Nazwiemy go "relationships". Ten kontroler będzie odpowiedzialny za tworzenie i usuwanie naszych relacji. Oraz nasz przycisk będzie odwoływał się bezpośrednio do akcji kontrolera. Dalej, stwórzmy ten kontroler:

bash

$ rails generate controller relationships

Możemy teraz dodać nowe akcje do kontrolera. Kiedy konstruujemy z REST API, używamy 7 (czasem 8) akcji:

nazwa akcji do czego używamy czy potrzbujemy szablonu HTML?
New Wyświetlanie formularza do tworzenia nowego rekordu Tak
Create Zapisywanie rekordu stworzonego z formularza z szablonu "new" i przekierowanie Nie, przekierowujemy
Edit Wyświetlanie formualrza do edycji istniejącego rekordu Tak
Update Zapisywanie zmian z formularza edycji z szablonu "edit" i przekierowanie Nie, przekierowujemy
Destroy Usuwanie rekordu i przekierowanie
Show Indywidualna strona rekordu (np. profil użytkownika, post) Tak
Index Lista rekordów Tak

Oczywiście możemy całkowicie lekceważyć tę strukturę. Możemy tworzyć własne akcje i nazywać je "vanilla_ice_cream" i sprawić by usuwały one wszystkie rekordy z bazy dancyh. Ale po co? Dobrą rzeczą w podążaniu za standardami jest to, że każdy może współpracować bez jakiegokolwiek wprowadzenia do projektu. Każdy wie jak Twoja aplikacja działa, i Ty będziesz znał strukturę aplikacji innych.

Przejdźmy z powrotem do listy akcji, których możemy uzyć i zobaczmy które z nich mogą być wykorzystane podczas twrozenia relacji.

Chcemy żemy przycisk "follow" działał na stronie profilowej użytkownika.

  • New - Nie. Nie potrzebujemy tego do relacji. Nie będziemy budować strony przeznaczonej do tworzenia relacji. Przycisk umieścimy na sotrnie profilowej użytkownika (kontroler Users, akcja "show").
  • Create - Tak. Zdefiniujemy akcję "create" do zapisywania relacji między użytkownikami i przekierowania po udanym zapisie.
  • Edit - Nie. Nie potrzebyjemy edytować relacji. Są podwójne - albo jesteśmy w relacji, albo nie.
  • Update - Nie. Ponieważ nie będziemy podsumowywać zmian, nie musimy nic zapisywać.
  • Destroy - Tak. Kiedy użytkownik zdecyduje, że nie chce być z kimś przyjacielem. Usuniemy tę relację.
  • Show - Nie. Nie potrzebujemy indywidualnych stron dla relacji. Może kiedyś będziesz chciał zmienić to zachowanie.
  • Index - Tak. Będziemy wyświetlać listę relacji użytkowników.

To uproszcza sprawy, nie będziemy uzywać wszystkich akcji. Możemy wybrać te, które, w naszym wypadku, mają sens.

Najpierw upewnijmy się, że mamy rutery, które mapują akcje, nad którymi zamierzamy pracować.

config/routes.rb

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

Teraz możemy kontynuować i zdefiniować akcje kontrolera, krok po kroku:

app/controllers/relationships_controller.rb

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

Zdefiniowalismy akcję create, ale jak na razie nie wykonuje ona rzadnej zaawansowanej pracy. Zwyczajnie wyświetlimy wszystkie parametry podsumowane przyciskiem (który dodamy). Wyświetlimy je jako zwykły tekst. Teraz potrzebujemy formularza, który będzie komunikował się z tą akcją. Dodamy go do strony profilowej użytkownika. Skopiuj kod od linii 6 do 9:

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>

Jak widzisz, nasz formularz zawiera dwa elementy: ukryte pole z identyfikatorem użytkownika, na którego stronie się znajdujemy i przycisk. Ponieważ pole id będzie automatycznie wypełnione, cały formularz wygląda jak pojedynczy przycisk. Przejdź teraz do http://0.0.0.0:3000/users/  i "zaproś" któregoś użytkownika.

listing parameters submitted by form

Wiemy, że formularz wysyła dane do właściwej akcji kontrolera. Nie martw się bardzo o składnię formularza. Zagłębimy się w szczegóły w przyszłych tutorialach.

Możemy teraz dokończyć naszą akcję tak, by robiła to co do niej należy:

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

Możemy teraz tworzyć relacje, chcemy także zmieniać napis "Invite" na "Stop following" jeżeli użytkownik jest w relacji z innym użytkownikiem. Dla ujawnienia, możemy to zrobić na więcej niż jeden sposób:

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 %>

Spójrzmy na powyższy kod. Zaczynamy od: <% @relationship = current_user.sent_invites.where(invited_id: @user.id).first %>. Ten kod sprawdza czy w bazie danych jest jakiś sent_invites (relacje), gdzie invited_id jest takie same co ID użytkownika, na którego stronie jesteśmy.

Pisanie tego tym sposobem jest uważane za niezbyt dobry zwyczaj, ale obecnie sprawi to łatwiejszym do zrozumienia pisany kod.

Później mamy wyrażenie if/else, które sprawdza czy zaproszenie zostało wysłane, czy nie, następnie wyświetla albo przycisk "invite", albo link "stop following".

Możemy teraz dodać akcję "destroy" do usuwania relacji.

app/controllers/relationships_controller.rb

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

I link do powyższego kodu:

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 %>

Wciąż jest kilka problemów z tym kode:

  • Każdy, nawet nie zalogowany użytkownik, widzi przyciski.
  • Każdy, nawet nie zalogowany użytkownik, teoretycznie może tworzyć i usuwać relacje
  • Każdy może usuwać relacje, nawet jeżeli one do niego nie należą

Naprawmy te problemy, jeden po drugim.

Ukrywanie przycisku dla niezalogowanych użytkowników.

Ten jest łatwy:

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 %>

Ten kod naprawia problem z pojawianiem się przycisku dla niezalogowanych uzytkowników. Użyliśmy if current_user do sprawdzenia czy użytkownik, który przegląda stronę, jest zalogowany.

Ograniczanie dostępu

Ponieważ używamy Devise, możemy ograniczyć dostęp dla wszystkich niezalogowanych użytkowników dodając before_filter.

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController

   before_filter :authenticate_user!

   #exisitng code
end

To pozwoli tylko zalogowanym użytkownikom na interakcje z akcjami.

Pozwalanie tylko właścicielom na usuwanie ich własnych zaproszeń

Ponownie, ten jest dość prosty. Musimy zmodyfikować akcję destroy.

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

Lista przyjaciół

Ostatnią rzeczą w tym tutorialu jest dodanie listy ralacji. Aby ją dodać potrzbyjemy dedykowaną stronę. "Index" brzmi jak coś czego poszukujemy. Śmiało, dodajmy nową akcję do naszego kontrolera:

app/controllers/relationships_controller.rb

#existing code

def index
end

I stwórzmy odpowiadający szablon HTML - index.html.erb, umieśćmy go wewnątrz folderu "relationships" w "views":

app/views/relationships/index.html.erb

<h1>Connected users</h1>

Teraz, wewnątrz kontrolera musimy znaleźć wszystkie relacje dotyczące danego użytkownika (current_user). Później zmodyfikujemy to zachowanie żeby działało jak na Facebook lub LinkedIn. Na tę chwilę, działa jak na 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 %>

Upewnijmy się, że link do tej strony znajduje się w menu naszej aplikacji:

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>

Ponieważ ta strona będzie pokazywać relacje powiązane z current_user nie musimy ukrywać relacji innego typu. W przyszłych tutorialach popracujemy nad przepływem i procesem "akceptowania zaproszeń", wysyłaniem emaili z powiadomieniami i przegądaniem przyjaciół każdego uzytkownika.

Musisz się zalogować by móc oznaczyć tutorial jako ukończony żeby śledzić swój postęp



Dodaj komentarz

Możesz się zalogować by skomentować