Reputation: 329
I'm trying to implement a simple like/unlike function. All the examples I have seen here seem to apply to ajax or jquery. I.m still a beginner and I don't fully understand either yet, I just want a simple solution.
My idea is, I have books and I have users. Users can like books. So I have created a many-to-many association through a Like model. The Like model has a corresponding database with book_id and user_id columns. Thus:
class Book < ActiveRecord::Base
has_many :likes
has_many :users, through: :likes
class User < ActiveRecord::Base
has_many :likes
has_many :books, through: :likes
class Like < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
unfortunately, this is as far as my understanding goes. I do not know how to utilize this relationship to create likes and associate them with the appropriate book and user.
My idea is to present the user a 'like' button if he has not liked the book, or unlike if he hasn't. So basically, I want to have something this simple:
<% if user likes? %>
<div class="unlike_button"><%= link_to "Unlike", '#' %></div>
<% else %>
<div class="like_button"><%= link_to "Like", '#' %></div>
<% end %>
I used the # stub because I have no idea what route it should use. But whatever the solution is, I would like it to redirect to the same page with a flash notice saying "liked" or "Unlike". I already have div background images for the like_button and unlike_button classes, which is why I implement them as image links above.
Any sort of guidance or help, would be deeply appreciated. Thanks
UPDATE
I'm following Bennick's guidance here below but I', still stuck in the rails console. I figure there's no point moving forward if I'm getting errors in the console.
As suggested, I tried this in the console:
As I already have users I did user = User.find(1)
and book = Book.find(1)
But on the next line like = Like.create(:user => user, :book => book)
the returned a mass assign error. Can't mass-assign protected attributes: book, user
I figured maybe this would help attr_accessible :book_id, :user_id
in the like model but I still get errors. Am I missing something?
SOLVED
I finally got it to work! Using like = Like.create(:user => user.id, :book => book.id)
.
Upvotes: 2
Views: 1587
Reputation: 1917
Okay there are a number of items here. I'm going to walk you up the stack (model -> associations -> controller -> view -> router). Typically when you design a web application you start with the database layer and work your way up. So we will do that here.
Model
This is where you decide what database objects you need and create database tables to represent them. If you have not already, read the Rails Guide to migrations: http://guides.rubyonrails.org/migrations.html
In your case this setup would be appropriate:
class Book < ActiveRecord::Base
attr_accessible :title
has_many :likes
has_many :users, through: :likes
end
class User < ActiveRecord::Base
attr_accessible :name
has_many :likes
has_many :books, through: :likes
end
class Like < ActiveRecord::Base
attr_accessible :book, :user
belongs_to :book
belongs_to :user
end
Notice that we need include attr_accessible
so we don't get any mass-assignment errors. Please not that in Rails 4 this security feature has moved into the controller. For more on this see these or search the inter webs:
http://blog.teamtreehouse.com/rails-4-strong-paremeters
http://weblog.rubyonrails.org/2012/3/21/strong-parameters/
Associations
You should read the Rails guide on associations: http://guides.rubyonrails.org/association_basics.html
This will give you a good idea on how database objects (Active Record objects) interact with one another. In your question you have already set these up. Once an association is made Rails provides a number of methods used to access them. Here is an example rails console session: rails c
# Create a user
user = User.create(:name => "Ryan") # I'm assuming User just requires a name for simplicity
=> #<User id: 1, name: "Ryan">
# Create two a books
book = Book.create(:title => "Game of Thrones")
=> #<Book id: 1, title: "Game of Thrones">
book2 = Book.create(:title => "The Well-Grounded Rubyist")
=> #<Book id: 2, title: "The Well-Grounded Rubyist">
# Create a two likes from the books and the user record
like = Like.create(:user => user, :book => book)
=> #<Like id: 1, user_id: 1, book_id: 1>
like2 = Like.create(:user => user, :book => book2)
=> #<Like id: 2, user_id: 1, book_id: 2>
# Notice how the keys glue the associations
# Query a user's likes
user.likes.count
=> 2
user.likes
=> #<ActiveRecord::Associations::CollectionProxy [#<Like id: 1, user_id: 1, book_id: 1>, #<Like id: 2, user_id: 1, book_id: 2>]
# Query a user's books
user.books
=> #<ActiveRecord::Associations::CollectionProxy [#<Book id: 1, title: "Game of Thrones">, #<Book id: 1, title: "The Well-Grounded Rubyist">]
When in doubt play with the rails console. You will learn a ton from it.
Controller
In order for the end user to interact with your database objects a controller is necessary to facilitate the exchange. Again, read the associated Rails Guide: http://guides.rubyonrails.org/action_controller_overview.html If you have not guessed by now I highly recommend reading most of these.
In your cause we are creating like objects so lets make a likes controller:
rails g controller likes index
This will create the controller with an index action and view file.
# app/controllers/likes_controller.rb
class LikesController < ApplicationController
# This action will show our likes for a user.
# Lets assume you have an authentication system (ex Devise) that logs a user in and provides a `current_user` object
# GET /likes
def index
# Assign the logged in user to @user
@user = current_user
# Grab all of the books and put them into an array in @books
@books = Book.all
end
# This is our key action. We will use this action to create a Like
# POST /likes
def create
# Grab our book from the DB. Note that this syntax is for Rails 3.2 and below. Rails 4 uses something called Strong Parameters, but that is for another time.
book = Book.find(params[:book_id])
# Create a like
Like.create(:book => book, :user => current_user)
# redirect back to the Like index page and assign a flash
redirect_to likes_path, :notice => "You just liked the book #{book.title}"
end
# here is where we will destroy a Like
# DELETE /likes/:id
def destroy
# Get the like form the DB
like = Like.find(params[:id])
# destroy it
like.destroy
redirect_to likes_path, :notice => "You destroyed a like"
end
end
Router
The router is what connects external http requests to your controller actions. In your case all you will need is this:
# config/routers.rb
MyApp::Application.routes.draw do
resources :likes
end
This is a Rails shortcut that sets up 7 standard routes with associated helpers:
likes GET /likes(.:format) likes#index
POST /likes(.:format) likes#create
new_like GET /likes/new(.:format) likes#new
edit_like GET /likes/:id/edit(.:format) likes#edit
like GET /likes/:id(.:format) likes#show
PUT /likes/:id(.:format) likes#update
DELETE /likes/:id(.:format) likes#destroy
Do yourself a favor and read this guide: http://guides.rubyonrails.org/routing.html It will explain what these routes are and how they work. Rails follows REST as does most of the modern web development world.
View
In your view you will need a form for users to interact with. This form will send data to the application, specifically to your LikesController actions.
# app/views/likes/index.html.erb
# show your flash messages
<% flash.each do |name, msg| %>
<div class="alert <%= "alert-#{name}" %>">
<%= msg %>
</div>
<% end %>
<h1>Books you may or may not like</h1>
# For each book
<% @books.each do |book| %>
<% unless @user.books.include?(book) %> # Prob want to move this into a User instance method
# Create a like form if the user does not have a like for this book
<%= form_tag likes_path do %>
<%= hidden_field_tag 'book_id', book.id %>
# Clicking this sends a request: POST /likes with params of: book_id=123
<%= submit_tag "Like this book", :class => "like_button" %>
<% end %>
<% else %>
# Find the like. I'll admit there is probably a better way to do this but it's getting past my bed time.
<% like = book.likes.where(:user_id => @user.id).first %>
# Destroy the like associated with this book and user
<div class="unlike_button">
# Clicking this sends a request to: DELETE /likes/123
<%= link_to "destroy like", likes_path(like.id), :method => :delete %>
</div>
<% end %>
<% end %>
Conclusion
I hope this gives you some guidance.
In the future try and keep your questions more specific as this question covered a big area. I'm just starting to actively give back so I might have over done it. I received a ton of free guidance and help when first starting out. It's about time I return the favor.
Take your time and when you get an error, just post it into Google. You will probably end up with a Stack Overflow question.
Cheers!
Upvotes: 6