I am following the book Agile Web Development with Rails - A Pragmatic Guide. On Iteration C1, creating a cart, I am getting the following error:
NoMethodError in StoreController#display_cart
undefined method `items' for #<Hash:0xb5cf041c>
Extracted source (around line #15):
13 def display_cart
14 #cart = find_cart
15 #items = #cart.items
16 end
17
18 private
Here are the source files:
routes.rb
Rails.application.routes.draw do
get 'store' => 'store#index'
get 'add_item_to_cart' => 'store#add_to_cart'
get 'display_cart' => 'store#display_cart'
resources :products
store_controller.rb
class StoreController < ApplicationController
def index
#products = Product.salable_items
end
def add_to_cart
product = Product.find(params[:id])
#cart = find_cart
#cart.add_product(product)
redirect_to display_cart_path
end
def display_cart
#cart = find_cart
#items = #cart.items
end
private
def find_cart
session[:cart] ||= Cart.new
end
end
cart.rb
class Cart
attr_reader :items
attr_reader :total_price
def initialize
#items = []
#total_price = 0.0
end
def items
#items
end
def add_product(product)
#items << LineItem.for_product(product)
#total_price += product.price
end
end
line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :product
def self.for_product(product)
item = self.new
item.quantity = 1
item.product = product
item.unit_price = product.price
item
end
end
I reach the action display_cart from the add_to_cart action of StoreController. Even though I have a def items in cart.rb, why am I getting a NoMethodError?
Assuming you have stored cart id in the session you can do following changes in your method
def find_cart
Cart.find_by_id(session[:cart_id]) ||= Cart.new
end
Try this to return your Cart instance:
def find_cart
Cart.find(session[:cart]
rescue ActiveRecord::RecordNotFound
cart = Cart.new
session[:cart] = cart.id
end
This is almost the same code as in your book. Semantically you could call that function current_cart.
By storing only the carts id and using the ActiveRecord find() method, rescuing with calling Cart.new , you will always return a cart instance.
Edit
if you do not implement Cart as a Model, you will (logically) never be able to instantiate an object of it.
This is a good article about handling sessions in Ruby on Rails. As you will see, the session itself is a Hash by 'nature', further down you will find some alternative solutions for storing sessions in your rails app.
The problem is in
def find_cart
session[:cart] ||= Cart.new
end
your session[:cart] is a Hash, and thus does not get Cart.new, and when you call #cart.items it gets called on Hash not on a Cart object
In your display_cart method: you're doing #cart = find_cart which may return a hash instead of an object as in the find_cart methhod you're doing session[:cart] ||= Cart.new which may return a Hash.
Related
In a rails 5 e-commerce site, Carts from a classic shopping cart model have not been destroyed because of a badly written code, so now the heroku database has more than 10 000 rows of empty carts. If I destroy them from the heroku console, next time a user who already came to the site but gave up in the middle of the buying process will try to reach the site, he will get a 404 error like this:
ActiveRecord::RecordNotFound (Couldn't find Cart with 'id'=305)
Because of the cookies, obviously. The heroku DB has exceeded its allocated storage capacity, so I need to destroy the empty carts. (And fix the code, but this is not the question here).
Is there a way to do this smoothly?
class CartsController < ApplicationController
def show
#cart = #current_cart
total = []
#cart.order_items.each do |item|
total << item.product.price_cents * item.quantity.to_i
end
#cart.amount_cents_cents = total.sum
end
def destroy
#cart = #current_cart
#cart.destroy
session[:cart_id] = nil
redirect_to root_path
end
end
class OrdersController < ApplicationController
before_action :authenticate_user!
def create
#order = Order.new
total = []
#current_cart.order_items.each do |item|
total << item.product.price_cents * item.quantity.to_i
end
#order.amount_cents_cents = total.sum
if #order.amount_cents_cents == 0
redirect_to root_path
else
#current_cart.order_items.each do |item|
#order.order_items << item
item.cart_id = nil
end
#user = current_user
#order.user_id = #user.id
#order.save
Cart.destroy(session[:cart_id])
session[:cart_id] = nil
redirect_to order_path(#order)
end
end
class ApplicationController < ActionController::Base
before_action :current_cart
def current_cart
if session[:cart_id]
cart = Cart.find(session[:cart_id])
if cart.present?
#current_cart = cart
else
session[:cart_id] = nil
end
end
if session[:cart_id] == nil
#current_cart = Cart.create
session[:cart_id] = #current_cart.id
end
end
instead of using find which throws an exception if it does not find a record you can use find_by_id which returns nil. And if you get nil you can make the cart empty and display a message to the user that your cart is empty.
also, you can use rescue block to rescue from the exception thrown by find
On my online shop I would like that a user could start shopping without an account, he could select items and add them to his cart.
He would create his account just before the payment process, if he doesn't have any account yet.
Also I would like that order belongs_to user
In my order.rb I added,
belongs_to :user, optional: true
So I can create the order without user_id.
I update the order with the user_id in the payment create method.
I need this association because I want to retrieve users' orders.
In my application_controller.rb I have a method that set a cart
before_action :current_cart
def current_cart
#current_cart ||= ShoppingCart.new(token: cart_token)
end
helper_method :current_cart
private
def cart_token
return #cart_token unless #cart_token.nil?
session[:cart_token] ||= SecureRandom.hex(8)
#cart_token = session[:cart_token]
end
Once my user have paid, his order is recorded.
Also I found out that as I don't force the association with the user and the order, an empty order is created because of the current_cart in the application_controller...
here is the shopping_cart.rb model
class ShoppingCart
delegate :sub_total, to: :order
def initialize(token:)
#token = token
end
def order
#order ||= Order.find_or_create_by(token: #token, status: 0) do |order|
order.sub_total = 0
end
end
def items_count
order.items.sum(:quantity)
end
def add_item(product_id:, quantity:1 , size_id:)
#product = Product.find(product_id)
#size = Size.find_by(id: size_id)
#order_item = if order.items.where(product_id: product_id).where(size_id: size_id).any?
order.items.find_by(product_id: product_id, size_id: size_id)
else
order.items.new(product_id: product_id, size_id: size_id)
end
#order_item.price = #product.price_cents
#order_item.quantity = quantity.to_i
ActiveRecord::Base.transaction do
#order_item.save
update_sub_total!
end
CartCleanupJob.set(wait: 1.minutes).perform_later(order.id)
end
def change_qty(id:, quantity:1, product_id:, size_id:)
#size = Size.find_by(id: size_id)
#order_item = order.items.find_by(product_id: product_id, size_id: size_id)
#order_item.quantity = quantity.to_i
#order_item.save
update_sub_total!
end
def remove_item(id:)
ActiveRecord::Base.transaction do
order.items.destroy(id)
update_sub_total!
end
end
private
def update_sub_total!
order.sub_total = order.items.sum('quantity * price')
order.save
end
end
What should I do in order that my user can create his account right before the payment, and not to have an empty order created...?
In the order method of your ShoppingCart class, you use find_or_create_by which as the name implies, call the create method of the Order class . If you switch to find_or_initialize_by instead the new method will be called giving you an Order object, but not created in the database.
def order
#order ||= Order.find_or_initialize_by(token: #token, status: 0) do |order|
order.sub_total = 0
end
end
I am doing cart for cafe, everything works fine except increasing quantity. When I add a food to cart...it just dublicating the same food...instead of increase the quantity. I want to make the solution that will check...If the food already in cart...instead of adding it will just increase the quantity. Below is my controller that adds food to cart. Any help will be appreciated
def add_to_cart
add_to_session(params[:cafe], params[:food])
redirect_to about_cafe_path(Cafe.find(params[:cafe]))
end
def show_cart
#cart = session[:cart]
end
def clear_cart
session[:cart] = {}
redirect_to show_cart_path
end
def remove
destroy_food(params[:cafe], params[:food])
redirect_to :back
end
private
def add_to_session(cafe_id, food_id)
if session[:cart][food_id].present?
session[:cart][cafe_id][food_id].quantity += 1
else
session[:cart][cafe_id].push food_id
end
end
def destroy_food(cafe_id, food_id)
session[:cart][cafe_id].delete food_id
end
you should check if session[:cart][cafe_id][food_id].present? instead of if session[:cart][food_id].present?
Hi guys so i am attempting a "add to cart function". I keep receiving "cart undefined method or variable" error and i can't seem to figure out where i'm going wrong.
Here's my cart.rb model
class Cart < ActiveRecord::Base
has_many :tutors
def add_tutor(tutor_id)
tutor = Tutor.where('tutor_id = ?', tutor_id)
if tutor
Cart.tutors << tutor
end
save
end
end
Here's my carts_controller.rb
class CartsController < ApplicationController
def show
#cart = current_cart
end
def add_to_cart
current_cart.add_tutor(params[:tutor_id])
redirect_to tutors_path
end
end
methods defined in application_controller.rb
def current_cart
if session[:cart_id]
#current_cart ||= Cart.find(session[:cart_id])
end
if session[:cart_id].nil?
#current_cart = Cart.create!
session[:cart_id] = #current_cart.id
end
#current_cart
end
code in routes.rb for the button
post '/add_to_cart/:tutor_id' => 'carts#add_to_cart', :as => 'add_to_cart'
and the code for the button to add to cart
<%= button_to "Shortlist Tutor", add_to_cart_path(:tutor_id => :tutor_id), :method => :post %>
Whenever i try the "Shortlist Tutor" button, i receive a NoMethodError in CartsController#add_to_cart, undefined method `tutors' for Cart(id: integer):Class
and the highlighted error is on the cart.rb model file and the line being
Cart.tutors << tutor
Any help regarding this error is greatly appreciated. Thank you for your time and advice!
In Cart.tutors << tutor Cart is a class. To add tutors to Cart, Cart must be an object. In your case current_cart is an object but Cart is class. so replace Cart in cart.rb with self as shown below
class Cart < ActiveRecord::Base
has_many :tutors
def add_tutor(tutor_id)
tutor = Tutor.where('tutor_id = ?', tutor_id)
if tutor
self.tutors << tutor
end
save
end
end
I'm trying to call some ActiveRecord methods in OrdersController#new action.
I've tested the code in Rails Console and it works the way I intended.
But in the controller action it does not produce the same
order has_many cart_items
cart has_many cart_items
cart_items belong_to order cart_items
belong_to cart
cart_items belong_to product
product has_many cart_items
def new
#order.new
#order.cart_items = current_cart.cart_items
#order.save
current_cart.cart_items.destroy_all
end
Now current_cart is an application_controller method that checks if the current user has a shopping cart. If it does it pulls that cart from the database and if the user does not then it creates a new cart for the user. What I am trying to do here is when the user finalizes their order I'm trying to transfer the cart_items from current_cart to orders then clear the shopping cart.
When I do this in rails console, it gives me what I want. Order with cart_items that were in current_cart, and after I run destroy_all on the cart I have an empty active record association array.
When I test this in my controller both Order and Cart return an empty active association array.
What is going on here?
#application controller method of finding current_users cart
def current_cart
# if user is logged in
if current_user
#user = current_user
# checking user to see if account is confirmed and verified
if #user.confirmed_at != nil
# checking if user already has cart in cart database
if Cart.find_by(users_id: #user.id) != nil
# find a row in the database where users_id: equal to #user.id
# where clause does not work here
cart = Cart.find_by(users_id: #user.id)
session[:cart_id] = cart.id
cart.save
#establish Cart session cart for user
Cart.find(session[:cart_id])
else
# create a new Cart Object for user.assign current_user's id to cart object
cart = Cart.new
cart.users_id = #user.id
# save it to get cart id assign session[:cart_id] == cart.id
cart.save
session[:cart_id] = cart.id
end
end
end
end
class CartItemsController < ApplicationController
before_action :set_cart_item, only: [:show, :edit, :update, :destroy]
# scope for most_recent and subtotal
# find out if rails sorts on update column cuz this is annoying.
def create
# grabbing cart from application controller current_cart method
#cart = current_cart
# session[:cart_id] = #cart.id
# individual product items get added to cart item and added to cart and saved
#cart_item = #cart.cart_items.build(cart_item_params)
#cart.save
end
def update
#cart = current_cart
# finding cart_items by cart_id
#cart_item = #cart.cart_items.find(params[:id])
# #cart_items.order(:id)
#cart_item.update_attributes(cart_item_params)
#cart_items = #cart.cart_items.order(:id)
# redirect 'cart_show_path'
#cart.save
end
def destroy
#cart = current_cart
#cart_item = #cart.cart_items.find(params[:id])
#cart_item.destroy
#cart_items = #cart.cart_items
#cart.save
end
private
def set_cart_item
#cart_item = CartItem.find(params[:id])
end
def cart_item_params
params.require(:cart_item).permit(:cart_id, :product_id, :unit_price, :quantity, :total_price)
end
end
When you say you are transferring the cart_items from current_cart you are passing on the objects, you are not creating new cart_items (which means the database ids are same) and when you do current_cart.cart_items.destroy_all it is deleting them from the database. See ActiveRecord::Relation#destroy_all
For you use case, its enough if you just do
def new
#order.new
#order.cart_items = current_cart.cart_items
#order.save
current_cart.cart_items = []
end
Alright, figured it out.
I was using #order.save which was not processing the error message!!!!!!!!
After #order.save! it gave me the validation error in another model.
I commented that out and it worked.
I iterated current.cart_items and assigned the cart_id to nil and order_id to #order.id essentially clearing the cart and "transferring" the items over.
I couldn't figure out a way using destroy_all though. I think this is impossible like #Sri said.
Thanks alot!
So the end code was this:
#order = Order.create!(users_id: current_cart.users_id)
current_cart.cart_items.each do |item|
item.order_id = #order.id
item.save!
end
if #order.save
current_cart.cart_items.each do |item|
item.cart_id = nil
item.save!
end