Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement "Add to favorites" in Rails 3 & 4

I am building an app where users can create recipes, see all recipes created, view their own recipes in a member area and finally i would like for users to add "favorites" to their account.

I am new to Rails but have read through docs and this is my understanding of what it should look like in the backend. Could someone confirm that this looks correct or advise of any errors please, with explanations if I have done something wrong (which is probably the case)?

So this is my code:

User Model

has_many :recipes
has_many_favorites, :through => :recipes

Recipe Model

belongs_to :user
has_many :ingredients #created seperate db for ingredients
has_many :prepererations #created seperate db for prep steps

Favorite Model

belongs_to :user
has_many :recipes, :through => :user
#this model has one column for the FK, :user_id

Favorites Controller

def create
  @favrecipes =current_user.favorites.create(params[:user_id])
end

I then wanted to have a button to post to the db, so I have this:

<%= button_to("Add to Favorites" :action => "create", :controller => "favorites" %>

I think I am probably missing something in my routes but I am unsure.

like image 667
Richlewis Avatar asked Nov 05 '12 20:11

Richlewis


2 Answers

The particular setup you describe mixes several types of associations.

A) User and Recipe

First we have a User model and second a Recipe model. Each recipe belonging to one user, hence we have a User :has_many recipes, Recipe belongs_to :user association. This relationship is stored in the recipe's user_id field.

$ rails g model Recipe user_id:integer ...
$ rails g model User ...

class Recipe < ActiveRecord::Base
  belongs_to :user
end

class User < ActiveRecord::Base
  has_many :recipes
end

B) FavoriteRecipe

Next we need to decide on how to implement the story that a user should be able to mark favorite recipes.

This can be done by using a join model - let's call it FavoriteRecipe - with the columns :user_id and :recipe_id. The association we're building here is a has_many :through association.

A User  
  - has_many :favorite_recipes  
  - has_many :favorites, through: :favorite_recipes, source: :recipe

A Recipe
  - has_many :favorite_recipes  
  - has_many :favorited_by, through: :favorite_recipes, source: :user 
      # returns the users that favorite a recipe

Adding this favorites has_many :through association to the models, we get our final results.

$ rails g model FavoriteRecipe recipe_id:integer user_id:integer

# Join model connecting user and favorites
class FavoriteRecipe < ActiveRecord::Base
  belongs_to :recipe
  belongs_to :user
end

---

class User < ActiveRecord::Base
  has_many :recipes

  # Favorite recipes of user
  has_many :favorite_recipes # just the 'relationships'
  has_many :favorites, through: :favorite_recipes, source: :recipe # the actual recipes a user favorites
end

class Recipe < ActiveRecord::Base
  belongs_to :user

  # Favorited by users
  has_many :favorite_recipes # just the 'relationships'
  has_many :favorited_by, through: :favorite_recipes, source: :user # the actual users favoriting a recipe
end

C) Interacting with the associations

##
# Association "A"

# Find recipes the current_user created
current_user.recipes

# Create recipe for current_user
current_user.recipes.create!(...)

# Load user that created a recipe
@recipe = Recipe.find(1)
@recipe.user

##
#  Association "B"

# Find favorites for current_user
current_user.favorites

# Find which users favorite @recipe
@recipe = Recipe.find(1)
@recipe.favorited_by # Retrieves users that have favorited this recipe

# Add an existing recipe to current_user's favorites
@recipe = Recipe.find(1)
current_user.favorites << @recipe

# Remove a recipe from current_user's favorites
@recipe = Recipe.find(1)
current_user.favorites.delete(@recipe)  # (Validate)

D) Controller Actions

There may be several approaches on how to implement Controller actions and routing. I quite like the one by Ryan Bates shown in Railscast #364 on the ActiveRecord Reputation System. The part of a solution described below is structured along the lines of the voting up and down mechanism there.

In our Routes file we add a member route on recipes called favorite. It should respond to post requests. This will add a favorite_recipe_path(@recipe) url helper for our view.

# config/routes.rb
resources :recipes do
  put :favorite, on: :member
end

In our RecipesController we can now add the corresponding favorite action. In there we need to determine what the user wants to do, favoriting or unfavoriting. For this a request parameter called e.g. type can be introduced, that we'll have to pass into our link helper later too.

class RecipesController < ...

  # Add and remove favorite recipes
  # for current_user
  def favorite
    type = params[:type]
    if type == "favorite"
      current_user.favorites << @recipe
      redirect_to :back, notice: 'You favorited #{@recipe.name}'

    elsif type == "unfavorite"
      current_user.favorites.delete(@recipe)
      redirect_to :back, notice: 'Unfavorited #{@recipe.name}'

    else
      # Type missing, nothing happens
      redirect_to :back, notice: 'Nothing happened.'
    end
  end

end

In your view you can then add the respective links to favoriting and unfavoriting recipes.

<% if current_user %>
  <%= link_to "favorite",   favorite_recipe_path(@recipe, type: "favorite"), method: :put %>
  <%= link_to "unfavorite", favorite_recipe_path(@recipe, type: "unfavorite"), method: :put %>
<% end %>

That's it. If a user clicks on the "favorite" link next to a recipe, this recipe is added to the current_user's favorites.

I hope that helps, and please ask any questions you like.

The Rails guides on associations are pretty comprehensives and will help you a lot when getting started.

like image 79
Thomas Klemm Avatar answered Nov 09 '22 11:11

Thomas Klemm


Thanks for the guide, Thomas! It works great.

Just wanted to add that in order for your favorite method to work correctly you need to wrap the text in double quotes instead of single quotes for the string interpolation to function.

redirect_to :back, notice: 'You favorited #{@recipe.name}'

->

redirect_to :back, notice: "You favorited #{@recipe.name}"

https://rubymonk.com/learning/books/1-ruby-primer/chapters/5-strings/lessons/31-string-basics

like image 21
blood runner Avatar answered Nov 09 '22 11:11

blood runner