Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 3: Polymorphic liking of Entities by User, how?

Background:

I followed the tutorial here to setup a polymorphic User favorites data model in my application. This allows me to let a User make pretty much any Entity in the system which I add 'has_many :favorites, :as => :favorable' line to its model a favorite. I plan on using this to implement a Facebook style 'Like' system as well as several other similar systems.

To start off I added the favoritability to a Post model (each user can create status updates like on Facebook). I have it all done and unit tested so I know the data model is sound and functioning from either side of the relationship (User and Post).

Details:

  • I have a Home controller with a single index method and view.

  • on the index view I render out the posts for the user and the user's friends

  • I want the user to be able to like posts from their friends

  • The Posts controller has only a create and a destroy method with associated routes (not a full fledged resource) and through the Post method via AJAX posts are created and deleted without issue

Where I am stuck

  • How do I add the link or button to add the post to the user's Favorites?

  • According to the tutorial the way to create a new Favorite through the polymorphic association is to do it from the Post.favorites.build(:user_id => current_user.id). From this direction the build handles pulling out the Post's ID and TYPE and all I have to do is pass in the user's id

  • Do I use an AJAX form post to a Favorites controller with a Create and Destroy method similar to the Post controller?

  • I am still struggling to uncross the wires in my brain from ASP.Net N-Tier web application development over to Rails MVC. Hasn't been too bad until now ;)

  • I bet there are Gems out there that might do this but I need to learn and the best way is to suffer through it. Maybe a tutorial or sample code from someone who has implemented liking functionality within their application would be helpful.

Thanks in advance for the assistance!

like image 529
Schleichermann Avatar asked Jul 21 '11 15:07

Schleichermann


People also ask

How is polymorphic association set up in Rails?

The basic structure of a polymorphic association (PA)sets up 2 columns in the comment table. (This is different from a typical one-to-many association, where we'd only need one column that references the id's of the model it belongs to). For a PA, the first column we need to create is for the selected model.

What is a polymorphic relationship rails?

Polymorphic relationship in Rails refers to a type of Active Record association. This concept is used to attach a model to another model that can be of a different type by only having to define one association.

What is polymorphic association Ruby?

2.9 Polymorphic Associations A slightly more advanced twist on associations is the polymorphic association. With polymorphic associations, a model can belong to more than one other model, on a single association. For example, you might have a picture model that belongs to either an employee model or a product model.


1 Answers

Jaap, I appreciate your comment on my question. After writing the question I pretty much didn't want to wait because the real learning takes place through trial and error, so I errored it up ;)

It turns out that what you suggested was pretty much in line with exactly what I ended up doing myself (it's always nice to find out that what you decide to do is what others would do as well, I love the sanity check value of it all).

So here is what I did and it is all working through post-backs. Now I just need to implement AJAX and style it:

My favorite model because my Polymorphic Favorites model requires that an Entity can only be favorited once by a user I added to the validations 'Scopes' which indicate that for each attribute it has to be unique in the scope of the other 2 required attributes. This solves the issue of multiple favorites by the same user.

class Favorite < ActiveRecord::Base
  before_save :associate_user

  belongs_to :favorable
  belongs_to :user

  # Validations
  validates :user_id, :presence => true,
            :uniqueness => {:scope => [:favorable_id, :favorable_type], :message => "item is already in favorites list."}
  validates :favorable_id, :presence => true,
            :uniqueness => {:scope => [:user_id, :favorable_type], :message => "item is already in favorites list."}
  validates :favorable_type, :presence => true,
            :uniqueness => {:scope => [:favorable_id, :user_id], :message => "item is already in favorites list."}

  # Callbacks
  protected

  def associate_user
    unless self.user_id
      return self.user_id = session[:user_id] if session[:user_id]
      return false
    end
  end

end

My User Model (that which is relevant): I added 2 methods, the get_favorites which is the same as favorable one from the tutorial and a Favorite? method which checks to see if the Entity in question has already been added to the user's favorites.

class User < ActiveRecord::Base  
  # Relationships
  has_many  :microposts, :dependent => :destroy
  has_many  :favorites

  # Methods
  def favorite?(id, type)
    if get_favorites({:id => id, :type => type}).length > 0
      return true
    end
    return false
  end

  def get_favorites(opts={})
    # Polymorphic Favoritability: allows any model in the
    # application to be favorited by the user.
    # favorable_type
    type = opts[:type] ? opts[:type] : :topic
    type = type.to_s.capitalize

    # add favorable_id to condition if id is provided
    con = ["user_id = ? AND favorable_type = ?", self.id, type]

    # append favorable id to the query if an :id is passed as an option into the
    # function, and then append that id as a string to the "con" Array
    if opts[:id]
      con[0] += " AND favorable_id = ?"
      con << opts[:id].to_s
    end

    # Return all Favorite objects matching the above conditions
    favs = Favorite.all(:conditions => con)

    case opts[:delve]
    when nil, false, :false
      return favs
    when true, :true
      # get a list of all favorited object ids
      fav_ids = favs.collect{|f| f.favorable_id.to_s}

      if fav_ids.size > 0
        # turn the Capitalized favorable_type into an actual class Constant
        type_class = type.constantize

        # build a query that only selects
        query = []
        fav_ids.size.times do
          query << "id = ?"
        end
        type_conditions = [query.join(" AND ")] + fav_ids

        return type_class.all(:conditions => type_conditions)
      else
        return []
      end
    end
  end

end

My Micropost Model (that which is relevant): note the Polymorphic association in the has_many relationship titled :favorites.

class Micropost < ActiveRecord::Base
  attr_accessible :content

  # Scopes
  default_scope :order => 'microposts.created_at DESC'

  # Relationships
  belongs_to  :user
  has_many  :favorites, :as => :favorable # Polymorphic Association

  # Validations
  validates :content, :presence => true, :length => { :minimum => 1, :maximum => 140 }
  validates :user_id, :presence => true

end

My Micropost Form: as you can see I am passing in the entity that will be mapped to the Favorite model as a local variable to the 2 Favorite forms as 'local_entity'. This way I can pull out the ID and the TYPE of the Entity for the Polymorphic association.

<div class="post">
    <span class="value">
      <%= micropost.content %>
    </span>
    <span>
        <% if current_user.favorite?(micropost.id, micropost.class.to_s) %>
            <%= render :partial => 'favorites/remove_favorite', :locals => {:local_entity => micropost} %>
        <% else %>
            <%= render :partial => 'favorites/make_favorite', :locals => {:local_entity => micropost} %>
        <% end %>
    </span>
    <span class="timestamp">
        Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    </span>
    <div class="clear"></div>
</div>

My Make Favorite Form:

<%= form_for current_user.favorites.build do |f| %>
    <div><%= f.hidden_field :favorable_id, :value => local_entity.id %></div>
    <div><%= f.hidden_field :favorable_type, :value => local_entity.class.to_s %></div>
    <div class="actions"><%= f.submit "make favorite" %></div>
<% end %>

My Remove Favorite Form:

<%= form_for current_user.get_favorites(
                     {:id => local_entity.id,
                      :type => local_entity.class.to_s}),
                      :html => { :method => :delete } do |f| %>
    <div class="actions"><%= f.submit "remove favorite" %></div>
<% end %>
like image 52
Schleichermann Avatar answered Sep 27 '22 17:09

Schleichermann