Skip to content

Instantly share code, notes, and snippets.

@jennli
Last active February 19, 2016 21:41
Show Gist options
  • Save jennli/123e1302f4bdbf518f89 to your computer and use it in GitHub Desktop.
Save jennli/123e1302f4bdbf518f89 to your computer and use it in GitHub Desktop.

associate many to many through a third join model

  • generate the like model for awesome answers
rails g model like user:references question:references
rake db:migrate
  • go the question model to add in association:
  has_many :likes, dependent: :destroy
  has_many :users, through: :likes
  • go to user model to add in association:
has_many :likes, dependent: :destroy
# because the following line uses like_questions, which is not standard naming convention
# must include "source: :question" for rails to know that liked_questions is actually question
has_many :liked_questions, through: :likes, source: :question
  • and the like model should have the following association:
belongs_to :user
belongs_to :question
  • test association in rails console:
2.2.3 :001 > u = User.last
  User Load (0.8ms)  SELECT  "users".* FROM "users"  ORDER BY "users"."id" DESC LIMIT 1
+----+----------+----------+----------+----------+---------+----------+-------+
| id | first... | last_... | email    | passw... | crea... | updat... | admin |
+----+----------+----------+----------+----------+---------+----------+-------+
| 12 | William  | Wang     | willi... | $2a$1... | 2016... | 2016-... | false |
+----+----------+----------+----------+----------+---------+----------+-------+
1 row in set
2.2.3 :002 > q = Question.last
  Question Load (2.3ms)  SELECT  "questions".* FROM "questions"  ORDER BY "questions"."id" DESC LIMIT 1
+-----+---------+---------+---------+----------+----------+---------+---------+
| id  | title   | body    | crea... | updat... | view_... | cate... | user_id |
+-----+---------+---------+---------+----------+----------+---------+---------+
| 522 | Will... | How ... | 2016... | 2016-... | 5        | 5       | 12      |
+-----+---------+---------+---------+----------+----------+---------+---------+
1 row in set
  • the above is to set u and q as user and question.
  • there are 3 ways to create many to many associations in the database:
2.2.3 :003 > u.liked_questions << q
   (0.1ms)  BEGIN
  SQL (1.2ms)  INSERT INTO "likes" ("user_id", "question_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["user_id", 12], ["question_id", 522], ["created_at", "2016-02-18 21:11:39.741137"], ["updated_at", "2016-02-18 21:11:39.741137"]]
   (6.1ms)  COMMIT
  Question Load (1.2ms)  SELECT "questions".* FROM "questions" INNER JOIN "likes" ON "questions"."id" = "likes"."question_id" WHERE "likes"."user_id" = $1  [["user_id", 12]]
+-----+---------+---------+---------+----------+----------+---------+---------+
| id  | title   | body    | crea... | updat... | view_... | cate... | user_id |
+-----+---------+---------+---------+----------+----------+---------+---------+
| 522 | Will... | How ... | 2016... | 2016-... | 5        | 5       | 12      |
+-----+---------+---------+---------+----------+----------+---------+---------+
1 row in set
  • or
2.2.3 :005 > q.users << u
   (0.2ms)  BEGIN
  SQL (0.3ms)  INSERT INTO "likes" ("question_id", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["question_id", 522], ["user_id", 12], ["created_at", "2016-02-18 21:13:28.052414"], ["updated_at", "2016-02-18 21:13:28.052414"]]
   (1.2ms)  COMMIT
+----+----------+----------+----------+----------+---------+----------+-------+
| id | first... | last_... | email    | passw... | crea... | updat... | admin |
+----+----------+----------+----------+----------+---------+----------+-------+
| 12 | William  | Wang     | willi... | $2a$1... | 2016... | 2016-... | false |
| 12 | William  | Wang     | willi... | $2a$1... | 2016... | 2016-... | false |
+----+----------+----------+----------+----------+---------+----------+-------+
  • finally
2.2.3 :006 > u.liked_question_ids = [522]
  Question Load (0.2ms)  SELECT  "questions".* FROM "questions" WHERE "questions"."id" = $1 LIMIT 1  [["id", 522]]
[
    [0] 522
]
  • now add like to the question routes
  resources :questions do
    get   :search,    on: :collection
    patch :mark_done, on: :member
    post  :approve
    resources :answers, only:[:create, :destroy]
    resources :likes, only: [:create, :destroy]
  end
  • add in a like_for method in the question model
def like_for (user)
    likes.find_by_user_id user
  end
  • add in a like link
  <% like = @question.like_for(current_user) %>
  <% if like %>
  <%= link_to "Unlike", question_like_path(@question, like), method: :delete %>
  <% else %>
  <%= link_to "Like", question_likes_path(@question), method: :post %>
  <% end %>
  (<%= @question.likes.count %>)
  
  • then modify the likes_controller:
class LikesController < ApplicationController
  before_action :find_question, only:[:create, :destroy]
  def create
    # current_user.liked_questions << q
    like = Like.new(question: @question, user: current_user)
    if like.save
      flash[:notice] = "liked"
    else
      flash[:alert] = "unliked"
    end
    redirect_to @question
  end

  def destroy
    @like = Like.find params[:id]
    @like.destroy
    redirect_to question_path(@question), notice: "Un-liked!"
  end

  private
  def find_question
    @question = Question.find params[:question_id]
  end

end
end
  • add in the validation in the like model to make sure the same user can only like a question once
validates :user_id, uniqueness: {scope: :question_id}

  • brew install graphviz
  • gem 'rails-erd'
  • rake erd
  • open erd.pdf

Tagging

rails g model tag name
rails g model tagging tag:references question:references
rake db:migrate
  • in tagging file (tagging model)
class Tagging < ActiveRecord::Base
  belongs_to :tag
  belongs_to :question

  validates :tag_id, :question_id, presence:true
  validates :tag_id, uniqueness: {scope: :question_id}
end
  • in tag model
class Tag < ActiveRecord::Base
  has_many :taggings, dependent: :destroy
  has_many :questions, through: :taggings
  validates :name, presence: true, uniqueness: true
end
  • in question model
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings
  • in db/seeds.rb file
["Coffee", "Hockey", "Snow", "Bao Buns", "Money"].each do |tag|
  Tag.create(name: tag)
end
  • run db:seed to generate the tag names
  • now can test this in rails console
q = Question.last
q.tag_ids = [2,4]
  Tag Load (0.4ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (2, 4)
   (0.1ms)  BEGIN
  Tagging Exists (0.3ms)  SELECT  1 AS one FROM "taggings" WHERE ("taggings"."tag_id" = 2 AND "taggings"."question_id" = 622) LIMIT 1
  SQL (1.5ms)  INSERT INTO "taggings" ("question_id", "tag_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["question_id", 622], ["tag_id", 2], ["created_at", "2016-02-19 17:48:24.336224"], ["updated_at", "2016-02-19 17:48:24.336224"]]
  Tagging Exists (0.3ms)  SELECT  1 AS one FROM "taggings" WHERE ("taggings"."tag_id" = 4 AND "taggings"."question_id" = 622) LIMIT 1
  SQL (0.3ms)  INSERT INTO "taggings" ("question_id", "tag_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["question_id", 622], ["tag_id", 4], ["created_at", "2016-02-19 17:48:24.341420"], ["updated_at", "2016-02-19 17:48:24.341420"]]
   (4.5ms)  COMMIT
[
    [0] 2,
    [1] 4
]
2.2.3 :005 > q.tags
+----+----------+-------------------------+-------------------------+
| id | name     | created_at              | updated_at              |
+----+----------+-------------------------+-------------------------+
| 2  | Hockey   | 2016-02-19 17:43:48 UTC | 2016-02-19 17:43:48 UTC |
| 4  | Bao Buns | 2016-02-19 17:43:48 UTC | 2016-02-19 17:43:48 UTC |
+----+----------+-------------------------+-------------------------+
  • use collection_check_boses
<div>
  <%= f.collection_check_boxes(:tag_ids, Tag.order("name"), :id, :name) %>
</div>
  • in questions controller the permitted params need to have tags_id
params.require(:question).permit([:title, :body, :category_id, {tag_ids: []}])

Vote

  • User, Vote, Question models
  • Vote table has a column to store "thumb up" or "thumb down" boolean value
  • (Question can optionally have a column called vote count to show the count without querying the vote table)
  • some scenarios

screen shot 2016-02-19 at 11 45 01 am

  • start with creating a vote model
rails g model vote user:references question:references is_up:boolean
rake db:migrate
rails g controller votes
  • routes. votes is inside questions routes
resources :questions do
  resources :votes, only: [:create, :update, :destroy]
end
  • in the question show view
<% vote = @question.vote_for(current_user) %>
  <% if !vote %>
  <%= link_to "Vote Up", question_votes_path(@question, {vote: {is_up: true}}),
  method: :post %>

  <%= link_to "Vote Down", question_votes_path(@question, {vote: {is_up: false}}),
  method: :post %>
  <% elsif vote.is_up? %>
  <%= link_to "Remove Vote Up", question_vote_path(@question, vote),
  method: :delete %>
  <%= link_to "Vote Down", question_vote_path(@question, vote, {vote: {is_up: false}}),
  method: :patch %>
  <% else %>
  <%= link_to "Vote Up", question_vote_path(@question, vote, {vote: {is_up: true}}),  method: :patch %>
  <%= link_to "Remove Vote Down", question_vote_path(@question, vote),
  method: :delete %>

  <% end %>
  • vote controller
class VotesController < ApplicationController
  before_action :authenticate_user
  before_action :find_question

  def create
    vote          = Vote.new vote_params
    vote.user     = current_user
    vote.question = @question
    flash = (vote.save) ? {notice: "Voted!"} : {alert: "Try again!"}
    redirect_to question_path(@question), flash
  end

  def update
    vote     = current_user.votes.find params[:id]
    vote.update vote_params
    redirect_to question_path(@question), notice: "Vote updated"
  end

  def destroy
    vote = current_user.votes.find params[:id]
    vote.destroy
    redirect_to question_path(@question), notice: "Vote removed"
  end

  private

  def vote_params
    params.require(:vote).permit(:is_up)
  end

  def find_question
    @question = Question.find params[:question_id]
  end

end

displaying voting results

  • show view
(<%= @question.vote_result %>)
  • vote model
  def self.up_count
    where(is_up: true).count
  end

  def self.down_count
    where(is_up: false).count
  end
  • question model
  def vote_result
    votes.up_count - votes.down_count
  end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment