Add a Favoriting System to a Rails Project
Have you ever wanted to add a favoriting system to your Rails project? I just recently had a need for this and decided I'd share how I did it.
The Setup
I've been working on a new project at work recently, and I needed a way to allow users to favorite projects. Now, this is something that is quite common with a lot of web applications today and I figured there would be ample documentation on the best way to do this. Unfortunately, I was mistaken.
While I did find a few articles, most of them seemed overly complex. So, with coffee in hand, I sat and pondered this problem for a while.
I have a users
model. I have a projects
model. I want users to be
able to favorite projects. Sounds pretty simple. I could easily do this with a HABTM
but I wanted it to be a bit more flexible. What if later, I want users to favorite users?
Okay, so a HABTM
wasn't the answer. What I really needed was a polymorphic
relationship. A user has many favorites and any other model can be favorited.
The Models
This sounded way too simple. So I set out to test my theory. The first thing I did was to
create the favorites
table:
bundle exec rails generate model favorite user:references favorited:references{polymorphic}
This creates a migration that looks like this:
class CreateFavorites < ActiveRecord::Migration
def change
create_table :favorites do |t|
t.references :favorited, polymorphic: true, index: true
t.references :user, index: true
t.timestamps
end
end
end
It also generated the model for me, which looks like this:
class Favorite < ActiveRecord::Base
belongs_to :favorited, polymorphic: true
belongs_to :user
end
After running rake db:migrate
, I moved onto the user model. I wanted to be able
to get all the favorites, but I also wanted a way of returning just favorited projects. This
will come in handy later when I start adding other models to the favoriting system. Again,
this is pretty simple to do:
class User < ActiveRecord::Base
has_many :favorites
has_many :favorite_projects, through: :favorites, source: :favorited, source_type: 'Project'
end
That is literally all you have to do with regards to the models. You can now CRUD favorites.
If the favorited
relationship happens to be a project, on the user object you can
now call user.favorite_projects
and it will return only favorited projects. Neat.
The Controller
I didn't want to muck up the users controller or the projects controller with favoriting details. The act of creating a "favorite" isn't really the responsibility of either the user or the project.
Instead, I created a completely separate resource called favorite_projects
. In
the future, I will probably make this more generic, but for now, users will only be able to
favorite projects. I had to add a new line in the routes file to make this happen and also
wanted to ensure that only create
and destroy
actions were
supported:
...
resources :favorite_projects, only: [:create, :destroy]
...
Finally, I created the controller which would use the current_user
method to
set the user for the favorite instance. For the create action, I passed a param called
project_id
since an id
in the params
hash for create wouldn't work. For that
reason, I have to find the project based on either params[:id]
or
params[:project_id]
. Here is the entire FavoriteProjectsController
:
class FavoriteProjectsController < ApplicationController
before_action :set_project
def create
if Favorite.create(favorited: @project, user: current_user)
redirect_to @project, notice: 'Project has been favorited'
else
redirect_to @project, alert: 'Something went wrong...*sad panda*'
end
end
def destroy
Favorite.where(favorited_id: @project.id, user_id: current_user.id).first.destroy
redirect_to @project, notice: 'Project is no longer in favorites'
end
private
def set_project
@project = Project.find(params[:project_id] || params[:id])
end
end
In Closing
Now, all I have to do in order to use this feature is generate a create or delete URL:
<%- unless current_user.favorite_projects.exists?(id: @project.id) -%>
<%= link_to 'Add to favorites', favorite_projects_path(project_id: @project), method: :post %>
<%- else -%>
<%= link_to 'Remove from favorites', favorite_project_path(@project), method: :delete %>
<%- end -%>
The best part about this is that I can easily modify this to accept any other model in my app that should have favoriting. I hope this is helpful to anyone reading this. Let me know if you use this technique or if I missed anything.