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.
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.
The Rails guides on associations are pretty comprehensives and will help you a lot when getting started.
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
This thread was super helpful!!
Thank you!
Don't forget to include the = in the form tags.
<% 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 %>
The selected answer is really good however I can't post a comment and I really do have an question about above. How can you limit a user to have one favourite recipe? With the above answer a user can continue pressing favorite and many entries will be created in the database...
Related
I am building an eCommerce website using rails 5 and activeadmin gem to manage my dashboard. I have a product and a category model in a many to one relationship.
class Product < ApplicationRecord
before_destroy :not_referenced_by_any_line_item
belongs_to :category
has_many :line_items, dependent: :destroy
has_many :reviews, dependent: :destroy
def self.search(search)
all.where("lower(title) LIKE :search", search: "%#{search}%")
end
private
def not_referenced_by_any_line_item
unless line_items.empty?
errors.add(:base, "line items present")
throw :abort
end
end
end
class Category < ApplicationRecord
has_many :products, dependent: :destroy
def self.search(search)
all.where("lower(category_name) LIKE :search", search: "%#{search}%")
end
end
I then registered the models to the activeadmin dashboard as below
ActiveAdmin.register Product do
permit_params :title, :description, :availability,
:price, :photo_link, :category_id, :advert, :pictureOne,
:pictureTwo, :pictureThree
end
ActiveAdmin.register Category do
permit_params :category_name, :photos
end
I can now select a product category on the project form when creating a product but the problem is, instead of a category name or any other field to display on the project category form input field so that you know exactly which category you are selecting,an abject is being displayed making it difficult to know which category you are selecting. display of dropdown of product category input form field:
ActiveAdmin's default functionality is to look for a name field on a given model when deciding what to render as the record's identifier. If the model doesn't have a name field, ActiveAdmin doesn't how else to let you which record you're dealing with besides being able to show you a stringified mess of the location where that record is in memory (It's the same string you'd get if you did Category.first.to_s in the console).
To get the ActiveAdmin to recognize the name, you have to override the default edit form it creates for you so you can customize the select label.
You'll add all the fields you want to be editable to the form. When you get adding the input for the category, you specify that you want that field to be a select and you can customize the select's label, like so:
# app/admin/product.rb
ActiveAdmin.register Product do
permit_params :title, :description, :availability,
:price, :photo_link, :category_id, :advert, :pictureOne,
:pictureTwo, :pictureThree
form do |f|
f.inputs do
# Add a form input for the category
#
# This approach also allows you to specify which categories you
# allow to be selected, in the "collection" attribute.
#
# Inside the "map" call, you have the proc return an array with the first item
# in the array being the name of the category (the label for the select)
# and the second item being the category's ID (the select's value)
f.input :category_id, label: 'Category', as: :select, collection: Category.all.map{ |c| [c.category_name, c.id]}
# Then add other inputs
f.input :title
f.input :description
f.input :availability
# ...
# (Add f.input for the rest of your fields)
end
f.actions
end
end
You'll follow similar methods when you need to render the name a category in other places in ActiveAdmin.
If it's not too much trouble, you'll probably be better off renaming category_name on your Category model to just name. That way, you'll be fighting with ActiveAdmin a lot less and won't need to make customizations like this as much.
I'm new to Rails 5 and I'm trying to model the following scenario using Devise and CanCanCan: a store application.
Relevant figures are: Admin, StoreManager, StoreOfficer, Customer, Technician.
Admin creates StoreManager.
StoreOfficer can create Customer's information to register new Customers.
StoreOfficer can see the complete list of all Customers with relative information
StoreOfficer can see the complete list of all Technicians with relative information
StoreManager can see the complete list of all StoreOfficers with relative information
StoreManager can enable StoreOfficers to use the system and edit their information
StoreManager can enable Customers created by StoreOfficers to use the system
StoreManager can see the complete list of all Customers with relative information
There are also some other paths, but it's possible for me to develop them as these presented cases are done.
Any help/tutorial please?
Thanks,
FZ
After trying some different things, I created the following satisfying schema, which I post here for others so that they can use something solid:
Class User<ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
belongs_to :userable, polymorphic: true
end
Then all the other roles classes similar to the following one:
class Customer < ApplicationRecord
belongs_to :technician
belongs_to :store_officer
has_one :user, as: :userable, dependent: :destroy
has_one :address, as: :addressable, dependent: :destroy
accepts_nested_attributes_for :user, :address
end
Accordingly, I created all the needed fields using migrations in the database.
Then I put in every controller the code before_action :authenticate_user!.
After populating the db a bit, I can now login with different users inserted using rake db:seed.
This is my routes.rb file:
Rails.application.routes.draw do
devise_for :users
resources :store_managers
resources :store_officers
resources :items
resources :technicians
resources :customers
resources :addresses
resources :users
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
What do you all think about this setup?
Now I want to introduce CanCanCan to my project. Please validate the following steps I'm going to do:
Run rails g cancan:ability
Put all the rules in the ability.rb file:
if user.admin?
can :manage, :all
elsif user.store_manager?
can :read, Customer
can :create, Customer
can :update, Item do |item|
item.try(:user) == user
end
can :destroy, Item do |item|
item.try(:user) == user
end
elsif user.store_officer?
can :read, Customer
end
.
.
.
Create the methods in user.rb model to check roles:
def customer?
self.userable_type == Customer
end
Put load_and_authorize_resource in every controller
Do you think this approach is the correct one? Or maybe I should put methods/definitions in other files?
Thanks,
FZ
EDIT
I am new to rails and got stuck at this step.Researched whatever I could on the internet but could not fix this. Please help! I am using Rails 3.2.13.
This is how my models look now. Excuse me for the typo, if any, as this is a made up example. cleaned up a bit but again the same problem. Could be bug not sure.
I have 3 Models:
1.Cuisine (example Thai/Mexican/Italian)
class Cuisine < ActiveRecord::Base
has_many :testers, :through => :ratings
has_many :ratings, :inverse_of => :cuisine
2.Testers
class Tester < ActiveRecord::Base
has_many :cuisines, :through => :ratings
has_many :ratings, :inverse_of => :tester
3.Rating (note:had the inverse_of here too but did not work)
class Rating < ActiveRecord::Base
belongs_to :tester
belongs_to :cuisine
testers_controller
class TestersController < ApplicationController
def update ##TO DO: SAVE (tester + cuisine IDS) IN THE ratings/JOIN TABLE
#tester = Tester.find(params[:id])
#tester.ratings.create
render text: 'Success'
end
This is form in the view. I am not using / rendering any partials for this exercise.
<%= form_for :rating, url: ratings_path do |f| %>
<h3>Review:</h3>
<% for cuisine in Cuisine.find(:all) %>
<div>
<%= check_box_tag("tester[cuisine_ids][]", cuisine.id) %>
<%= cuisine.cuisine_name %>
<% end %>
</div>
<p><%= f.submit %></p>
<% end %>
The development log shows as below.
Started PUT "/testers/3" for 127.0.0.1 at 2014-11-27 16:53:31 -0700
Processing by TestersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"5awCMjqwUSHaByj1XFDs5UKZUjyvMoigB88NZCFWgSE=", "tester"=> {"cuisine_ids"=>["3", "6"]}, "commit"=>"Update Tester", "id"=>"3"}
User Load (0.3ms) SELECT `testers`.* FROM `testers` WHERE `testers`.`id` = 3 LIMIT 1
Cuisine Load (0.4ms) SELECT `cuisines`.* FROM `cuisines` WHERE `cuisines`.`id` = 3 LIMIT 1
(0.1ms) BEGIN
SQL (0.2ms) INSERT INTO `ratings` (`created_at`, `created_by`, `cuisine_id`, `updated_at`, `tester`, `tester_id`) VALUES ('2014-11-27 23:53:31', NULL, NULL, '2014-11-27 23:53:31', NULL, 3)
(0.4ms) COMMIT
Rendered text template (0.0ms)
Completed 200 OK in 15ms (Views: 0.3ms | ActiveRecord: 3.8ms)
Couple of issues here.
1. Cuisine_ids are not getting inserted in the ratings table
2. If I have combination of tester_id =1 and cuisine_ids = [2,3] already in the join table then it does nothing. I would like to insert again the same values. that is I would like to allow insert statement to work for inserting duplicate entries. That is how my ratings work.
3. If I have combination of tester_id= and cuisine_ids= [1,2,3] and if I select cuisine_ids=[2,3], then it somehow, rails deletes the cuisine_ids[1,2,3] and again inserts [2,3]. so firstly it executes
1.DELETE from ratings where tester_id=1 (and then runs the insert again)
All I want to do is to save the records which the users select using check boxes in the join table. I want to allow duplicates (for tester_id and cuisine_ids combination) in that join table. This join table might resemble to transaction tables i.e. like one product might have repeated/recurring orders by same customer. However, the entire row will still remain unique because rails has its own primary key on each table.
Let me know if you need more information. Someone please help!!!!
Thanks
I need to know more about your data model but I can provide some general guidance here.
class Cuisine < ActiveRecord::Base
attr_accessible :cuisine_name, :id, :cuisine_id
attr_accessor :tester, :cuisine_ids
has_many :testers, :through => :ratings
has_many :ratings
First, adding ":id" to attr_accessible doesn't make sense. You don't need it as it would give people the ability to update a record's id. I'm not sure what "cuisine_id" is, it doesn't make sense that it would be in the cuisines table.
You definitely don't need attr_accessor. And you should add inverse relationships.
So here's where we're left:
class Cuisine < ActiveRecord::Base
attr_accessible :cuisine_name
has_many :ratings, :inverse_of => :cuisine
has_many :testers, :through => :ratings
Next up is Tester:
class Tester < ActiveRecord::Base
attr_accessible :tester_name, :id, :tester_id
attr_accessor :cusines
has_many :cusines, :through => :ratings
has_many :ratings
Again, your attr_accessible is confusing. You also have misspelled "cuisine". Finally, you don't need the attr_accessor and, again, it will actually "hide" the real "cuisines" relationship. Here's what we end up with:
class Tester < ActiveRecord::Base
attr_accessible :tester_name
has_many :ratings, :inverse_of => :tester
has_many :cuisines, :through => :ratings
Finally, ratings. This is simple and actually looks good, but it strikes me that there's probably at least some sort of "rating" attribute that should be in attr_accessors. Since I don't know what's there, let's just add inverse relationships:
class Rating < ActiveRecord::Base
belongs_to :tester, :inverse_of => :ratings
belongs_to :cuisine, :inverse_of => :ratings
Having those correct will make fixing the rest of it possible.
Looking at your ratings controller, it's kind of confusing. I have no idea what the "ratings" action should do. You're mixing plurals and singulars and all that. You have "cuisine" misspelled in another way there. Let's start with the view:
<h3>Review:</h3>
<% for cuisine in Cuisine.find(:all) %>
<div>
<%= check_box_tag("tester[cuisine_ids][]", cuisine.id) %>
<%= cuisine.cuisine_name %>
</div>
<% end %>
<p><%= f.submit %></p>
<% end %>
Presumably there's a "form_for" that you didn't include. I have done the minimal needed to get this to at least render properly. It's still not exactly what you want as it won't allow for updates.
Back to the controller, what is "ratings" supposed to do? Is that your "new" action?
class RatingsController < ApplicationController
def ratings
#ratings=Rating.new
end
def create
#tester= Tester.new(params[:tester])
params[:tester][:cuisine_ids].each do |tester|
#cus=#tester.ratings.build(:tester_id => tester)
#cus.save
end
end
That's a general cleanup. You still aren't saving "#tester" or doing anything else after the create. If you start with a general resource scaffold for testers you can do this pretty easily. But with these issues fixed you can at least start making some headway.
As I said in the comment above, though, you would be wise to put this aside, go through a tutorial, and then revisit this.
I am building a simple web app with a typical User model & Profile model. The user model has_one profile & the profile model belongs_to user. All is going pretty well as I am basically following Michael Hartl's tutorial (except he uses has_many for microposts).
So here's the deal. I create a user, then create a profile. I know both records exist b/c I verify their presence in my sqlite3 db browser. However, as soon as I try to render the show view by visiting -> localhost/3000/profiles/1, I get the error shown below. But the even weirder part is that now when I check my db, the profile record is gone. Please help!
Note: I have a feeling it has something to do with dependent destroy (b/c removing it will eliminate this issue), but I have no idea why. Furthermore, I think I want dependent destroy anyway. I don't want any stray profile records if there are no corresponding users, right?
Routes
resources :users
resources :profiles
Models
class User < ActiveRecord::Base
has_one :profile, dependent: :destroy
class Profile < ActiveRecord::Base
belongs_to :user
ProfilesController
def new
#profile = current_user.build_profile
end
def create
#profile = current_user.build_profile(params[:profile])
if #profile.save
flash[:success] = "Profile created!"
redirect_to root_path
else
render 'new'
end
end
def show
#profile = Profile.find(params[:id])
end
views/profiles/show.html.erb
<p>Display Name: <%= #profile.display_name %></p>
This is the error message I get when I try to visit -> localhost/3000/profiles/1
ActiveRecord::RecordNotFound in ProfilesController#show
Couldn't find Profile without an ID
This is the first time I've tried to add a tags field to a form so bear with me.
I'm using:
rails 3.2.8
Postgres 9.1
rocket_tag 0.5.6
squeel 1.0.9
I have installed the rocket_tag gem and run the various commands
rails generate rocket_tag:migration
rake db:migrate
rake db:test:prepare
to set everything up.
I've amended my Meeting model:
*Adding :tags and :tag_list to attr_accessible
*Adding attr_taggable :tags
*I've added a virtual attribute for displaying the tag list in the form field:
def tag_list
self.tags.join(",")
end
def tag_list=(new_tags)
self.tags = new_tags.split(/,[\s]*/).reject(&:empty?)
end
*I have added f.text_field :tag_list to my form view
This left me with my list page not working though. It said Association named 'taggings' was not found; perhaps you misspelled it?
User
has_many :meetings
has_many :meeting_dates, :through => :meetings
Meeting
belongs_to :user
has_many :meeting_dates
Meeting Dates
belongs_to :meeting
In my list page I list meeting dates using:
user_meeting_dates = #user.meeting_dates.includes(:meeting => :user)
To get past the taggings error I seem to need to have the following in my Meeting Dates model:
has_many :taggings, :through => :meeting
has_many :tags, :through => :taggings
Can someone explain why I need these here and whether there's a way to tidy this up any more, it doesn't seem like it's right.
Should it not be possible to do something like:
user_meeting_dates = #user.meetings.meeting_dates
Thanks
Once I've got this working correctly I can move on to apply Select2 to my tags field but I need to get it working just from a plain text field first.