So I've built a system of products and a shopping cart in my rails app. The goal I have is to add ids of the saved products from a cart to the user model. So in my cart view page there is a list of all added products in a cart and I want to add a save button which will save those products by their ids to the columns in users table. As an example, if current_user ads three products in the cart with ids 1,2,3 and clicks on "Save" button in a cart, I want to be able to save those three ids by integers to the three columns: product_one, product_two, product_three of the current_user.
So far these are my models:
class Item < ActiveRecord::Base
has_one :cart
end
class User < ActiveRecord::Base
has_one :cart
has_many :items, through: :cart
end
class Cart < ActiveRecord::Base
belongs_to :user
belongs_to :item
validates_uniqueness_of :user, scope: :item
end
My controllers:
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]
respond_to :html, :json, :js
def index
#items = Item.where(availability: true)
end
def show
end
def new
#item = Item.new
end
def edit
end
def create
#item = Item.new(item_params)
#item.save
respond_with(#item)
end
def update
#item.update(item_params)
flash[:notice] = 'Item was successfully updated.'
respond_with(#item)
end
def destroy
#item.destroy
redirect_to items_url, notice: 'Item was successfully destroyed.'
end
private
def set_item
#item = Item.find(params[:id])
end
def item_params
params.require(:item).permit(:name, :description, :availability)
end
end
my cart controller:
class CartController < ApplicationController
before_action :authenticate_user!, except: [:index]
def add
id = params[:id]
if session[:cart] then
cart = session[:cart]
else
session[:cart] = {}
cart = session[:cart]
end
if cart[id] then
cart[id] = cart[id] + 1
else
cart[id] = 1
end
redirect_to :action => :index
end
def clearCart
session[:cart] = nil
redirect_to :action => :index
end
def index
if session[:cart] then
#cart = session[:cart]
else
#cart = {}
end
end
end
And I'm using Devise for authentication..
I think you may have misunderstood the Rails relations and how to use them. As the methods to define relation are pretty much literal, take a good look at your models and 'read' them.
An item has one cart
A cart belongs to a item
Does it make sense that an item has one cart? Wouldn't make more sense to a cart to have an item, or several?
A cart has one or more items
One item belongs to a cart
And then, you just translate that into rails methods:
class User < ActiveRecord::Base
has_one :cart
end
class Cart < ActiveRecord::Base
belongs_to :user #carts table must have a user_id field
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :cart #items table must have a cart_id field
end
Now, let's return to the literals. So, if I have a user and want to know what items he has in a cart, what do I do?
I know a user has one cart
I know that a cart has one or more items
So, to recover the items that a user has in a cart:
user.cart.items
And answering your original question, how to save the items to a user? You don't need to. If the user has a cart and this cart has items then, automatically, user has items (accessing them through the cart, as stated above).
Related
I have the following code letting a user to create a new album through a join table with an extra params (creator).
In order to do it, my controller does 2 requests (one for creating the album object and the collaboration object / the other to update the collaboration object with the extra params).
I would like to know if there is a way to do this call with only one request. (add the extra "creator" params in the same time than the album creation)
Thank you.
albums_controller.rb
class AlbumsController < ApplicationController
def new
#album = current_user.albums.build
end
def create
#album = current_user.albums.build(album_params)
if current_user.save
#album.collaborations.first.update_attribute :creator, true
redirect_to user_albums_path(current_user), notice: "Saved."
else
render :new
end
end
private
def album_params
params.require(:album).permit(:name)
end
end
Album.rb
class Album < ApplicationRecord
# Relations
has_many :collaborations
has_many :users, through: :collaborations
end
Collaboration.rb
class Collaboration < ApplicationRecord
belongs_to :album
belongs_to :user
end
User.rb
class User < ApplicationRecord
has_many :collaborations
has_many :albums, through: :collaborations
end
views/albums/new
= simple_form_for [:user, #album] do |f|
= f.input :name
= f.button :submit
You can just add associated objects on the new album instance:
#album = current_user.albums.new(album_params)
#album.collaborations.new(user: current_user, creator: true)
When you call #album.save ActiveRecord will automatically save the associated records in the same transaction.
class AlbumsController < ApplicationController
def new
#album = current_user.albums.new
end
def create
#album = current_user.albums.new(album_params)
#album.collaborations.new(user: current_user, creator: true)
if #album.save
redirect_to user_albums_path(current_user), notice: "Saved."
else
render :new
end
end
private
def album_params
params.require(:album).permit(:name)
end
end
You are also calling current_user.save and not #album.save. The former does work due to fact that it causes AR to save the associations but is not optimal since it triggers an unessicary update of the user model.
I have a question for rails.
I have a User controller,
I have a Product controller.
I have a user id references in product:db.
How to puts User.product number in Html?
Firstly you need configure devise gem for authentication to your user model to add user_id column to your products table.
rails g migartion add_user_id_to_products user_id:integer:index
In your users model
class User < ApplicationRecord
has_many :products
end
In your products model
class Products < ApplicationRecord
belongs_to :user
end
As your user and products are associated through has_many and belongs_to.
you can as below in the products controller
class ProductsController < ApplicationController
def index
#products = Product.all
end
def new
#product = Product.new
end
def create
#product = current_user.products.build(product_params)
if #product.save
redirect_to edit_product_path(#product), notice: "Saved..."
else
render :new
end
end
private
def product_params
params.require(:product).permit( :title, :description, :category)
end
end
If the data is successfully saved into the database, you will find the user_id column of the products table filled with the id of the current_user.
To get all the products of a particular user
In your users controller show action
def show
#user_products = #user.products
end
The #user_products will have all the products belonging to the corresponding user.
In an E commerce Rails App I'm building products that is deleted from the ShoppingCart are not added back to the production model after deletion.
When I add Products to the Cart the App is using this controller below to decrease the number of products from the Product model( see the create method)
controllers/product_item_controller.rb
class ProductItemsController < ApplicationController
include CurrentCart
before_action :set_cart, only: [:create]
before_action :set_product_item, only: [:show, :destroy]
def create
#product = Product.find(params[:product_id])
#product_item = #cart.add_product(#product.id)
if #product_item.save
redirect_to root_url, notice:'Product added to Cart'
product = Product.find params[:product_id]
product.update_columns(stock_quantity: product.stock_quantity - 1)
else
render :new
end
end
private
def set_product_item
#product_item = ProductItem.find(params[:id])
end
def product_item_params
params.require(:product_item).permit(:product_id)
end
end
That is woking fine.
But when I delete the Cart it gets deleted but the products are not added to the products model. And I also get this messages : Invalid Cart
this is the carts_controller.rb
class CartsController < ApplicationController
before_action :set_cart, only: [:show, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
def new
#cart = Cart.new
end
def show
#images = ["1.jpg", "2.jpg", "3.jpg", "4.jpg", "5.jpg"]
#random_no = rand(5)
#random_image = #images[#random_no]
end
def destroy
#cart.destroy if #cart.id == session[:cart_id]
session[:cart_id] = nil
product = Product.find params[:product_id]
product.update_columns(stock_quantity: product.stock_quantity + 1)
redirect_to root_url, notice: 'Your Cart is Empty'
end
def remove
cart = session['cart']
item = cart['items'].find { |item| item['product_id'] == params[:id] }
product = Product.find(item['product_id'])
product.update_columns(stock_quantity: product.stock_quantity + 1)
if item
cart['items'].delete item
end
redirect_to cart_path
end
private
def set_cart
#cart = Cart.find(params[:id])
end
def cart_params
params[:cart]
end
def invalid_cart
logger_error = 'You are trying to access invalid cart'
redirect_to root_url, notice: 'Invalid Cart'
end
end
I Can't see what is wrong with this code and why the products are not added to the product.rb after being deleted from the Cart.
Am I missing something here? Could someone advise me here?
Below are other relevant models and controllers
products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
def show
end
def search
#product = Product.search(params[:query]).order("created_at DESC")
#categories = Category.joins(:products).where(:products => {:id => #product.map{|x| x.id }}).distinct
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
#product = Product.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:title, :description, :price_usd, :price_isl, :image, :category_id, :stock_quantity, :label_id, :query)
end
end
Cart.rbmodel
class Cart < ActiveRecord::Base
has_many :product_items, dependent: :destroy
def add_product(product_id)
current_item = product_items.find_by(product_id: product_id)
if current_item
current_item.quantity += 1
else
current_item = product_items.build(product_id: product_id)
end
current_item
end
def total_price_usd
product_items.to_a.sum{|item| item.total_price_usd}
end
def total_price_isl
product_items.to_a.sum{|item| item.total_price_isl}
end
end
product.rbmodel
Class Product < ActiveRecord::Base
belongs_to :category
belongs_to :label
has_many :product_item, :dependent => :destroy
#before_destroy :ensure_not_product_item
validates :title, :description, presence: true
validates :price_usd, :price_isl, numericality: {greater_than_or_equal_to: 0.01}
validates :title, uniqueness: true
has_attached_file :image, styles: { medium: "500x500#", thumb: "100x100#" }
validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/
#def ensure_not_product_item
# if product_item.empty?
# return true
# else
# errors.add(:base, 'You have Product Items')
# return false
# end
#end
def self.search(query)
where("title LIKE ? OR description LIKE ?", "%#{query}%", "%#{query}%")
end
end
You are rescuing from ActiveRecord::RecordNotFound
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
But you're probably rescuing inappropriately... from the Product.find... in the destroy method. I'm not sure why you would expect the product_id to be in params.
Your code...
def destroy
#cart.destroy if #cart.id == session[:cart_id]
session[:cart_id] = nil
product = Product.find params[:product_id]
product.update_columns(stock_quantity: product.stock_quantity + 1)
redirect_to root_url, notice: 'Your Cart is Empty'
end
A better alternative might be...
def destroy
if #card.id == session[:cart_id]
#cart.product_items each do |product_item|
product_item.product.update_columns(stock_quantity: product_item.product.stock_quantity + 1)
end
#cart.destroy
end
end
However this might better be done as a before_destroy action for product_item model, so that destroying a product_item will automatically increment the stock total.
I'm not going to give a line by line solution as there are quite a few points about this application that not quite right and require a bit of rethinking. Lets look at how a shopping cart commonly is done.
The models:
class User < ApplicationRecord
has_many :orders
has_many :products, through: :orders
def current_order
orders.find_or_create_by(status: :open)
end
end
class Order < ApplicationRecord
enum status: [:in_cart, :processing, :shipped]
belongs_to :user
has_many :line_items
has_many :products, through: :line_items
end
# The join model between a Order and Product
# The name line item comes from the lines on a order form.
class LineItem < ApplicationRecord
belongs_to :order
belongs_to :product
end
class Product < ApplicationRecord
has_many :line_items
has_many :orders, through: :line_items
end
The naming here is not a mistake or sloppy copy pasting. A cart is only a concept in web app which exists as a "user aid" in creating an order.
The join between a Order and Product is commonly called a line-item. Note that we use has_many though: so that we can query:
User.find(1).order
Product.find(1).orders
Order.find(1).products
The Controllers
When building something as complicated as a checkout you will want to pay attention to the Single Responsibility Principle and KISS. Having many classes is not a bad thing. Having huge tangled controllers that do far too much is.
So for example create a controller that has adding and removing items from the cart as its sole responsibility.
# routes.rb
resource :cart
resources :line_items,
only: [:create, :destroy, :update] do
collection do
delete :clear
end
end
end
# app/controllers/line_items.rb
class LineItemsController < ApplicationController
before_action :set_cart
before_action :set_item
rescue_from Orders::NotOpenError, -> { redirect_to #order, error: 'Order is locked and cannot be edited' }
# Add an item to cart
# POST /cart/line_items
def create
#cart.product_items.create(create_params)
# ...
end
# Remove an item from cart
# DESTROY /cart/line_items/:id
def destroy
#item.destroy
if #item.destroyed?
redirect_to cart_path, success: 'Item removed.'
else
redirect_to cart_path, alert: 'Could not remove item.'
end
end
# Remove all items from cart
# DESTROY /cart/line_items
def clear
#order.line_items.destroy_all
if #order.items.count.zero?
redirect_to cart_path, success: 'All items destroyed'
else
redirect_to cart_path, alert: 'Could not remove all items.'
end
end
# Update a line in the order
# PATCH /cart/line_items/:id
def update
#line_item.update(update_params)
end
private
def set_order
#order = current_user.current_order
# Ensure that order is not processed in some way
raise Orders::NotOpenError unless #order.open?
end
def set_line_item
#line_item = #order.line_items.find(params[:id])
end
def create_params
params.require(:line_item).permit(:product_id, :quantity)
end
def update_params
params.require(:line_item).permit(:quantity)
end
end
Notice how nicely the path for route each clearly tells us what it does and how we can write a description of the controller in a single line without using the word and.
In addition to this you will want a ProductsController, CartController, OrderController, PaymentsController etc. each of should do a single job - and do it well.
Don't do it all in your controllers!
When we add a line item to a order the available stock of the product should of course decrease. This is a clear cut example of business logic.
In MVC business logic belongs in the model layer.
A user adding a item to the cart should only create a reservation. The actual inventory of a product should only be altered when the order is processed or ships:
# No callbacks needed!
class Product < ApplicationRecord
has_many :line_items
has_many :orders, through: :line_items
def reservations
line_items.joins(:order)
.where
.not(line_items: {
order: Order.statuses[:shipped]
})
.sum(:quantity)
end
def availibity
stock - reservations
end
end
You've got
before_action :set_cart, only: [:show, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :invalid_cart
As soon as the CartsController#destroy method is invoked the private method set_cart is called. What it tries to do is to initialize an instance variable #cart = Cart.find(params[:id]).
The first line of your #destroy method is #cart.destroy if #cart.id == session[:cart_id]. Isn't the #cart = Cart.find(params[:id]) a problem here? What is the value of params[:id]? I guess it's not the same as session[:cart_id] and might probably be a nil or some Intreger value by which the DB cannot find a Cart record, hence the error.
Edit 1:
The same applies to the product = Product.find params[:product_id] as Steve mentioned in his answer.
Max posted a very informative report on how it should be done properly. If you have the time stick to his answer and try to redesign your app in accordance to his suggestion.
I have a product.
I have an order.
I have a booking in between.
Whenever I make a booking from the product to the order it saves a new unique booking.
It should:
Save a new booking when it's the first made from this product.
Not make a new booking, but find and overwrite an old one, if the product has already been booked once.
If the product has already been booked on the order, but no changes are made, no database transactions are made.
def create
#order = current_order
#booking = #order.bookings.where(product_id: params[:product_id]).first_or_initialize
product = #booking.product
if #booking.new_record?
#booking.product_name = product.name
#booking.product_price = product.price
else
#booking.product_quantity = params[:product_quantity]
#booking.save
#order.sum_all_bookings
#order.save
end
Doesn't work.
Following worked:
def create
#booking = #order.bookings.find_by(product_id: params[:booking][:product_id])
if #booking
#booking.product_quantity = params[:booking][:product_quantity]
#booking.save
else
#booking = #order.bookings.new(booking_params)
#product = #booking.product
#booking.product_name = #product.name
#booking.product_price = #product.price
end
#order.save
end
Apparently I needed to grab the params, by adding [:booking] like in params[:booking][:product_id]. Anybody knows why?
You can try
#order.bookings.find_or_initialize_by(product_id: params[:product_id]).tap do |b|
# your business logic here
end
To avoid duplicates you should setup relations properly and using a database index to ensure uniqueness.
class Order
has_many :bookings
has_many :products, though: :bookings
end
class Booking
belongs_to :order
belongs_to :product
validates_uniqueness_of :order_id, scope: :product_id
end
class Product
has_many :bookings
has_many :orders, though: :bookings
end
The validation here will prevent inserting duplicates on the application level. However it is still prone to race conditions.
class AddUniquenessContstraintToBooking < ActiveRecord::Migration[5.0]
def change
add_index :bookings, [:order_id, :product_id], unique: true
end
end
However the rest of you controller logic is muddled and overcomplicated. I would distinct routes for update and create:
class BookingsController < ApplicationController
before_action :set_order, only: [:create, :index]
before_action :set_order, only: [:create, :index]
# POST /orders/:order_id/bookings
def create
#booking = #order.bookings.new(booking_params)
if #booking.save
redirect_to #order
else
render :new
end
end
# PATCH /bookings/:id
def update
if #booking.update(:booking_params)
redirect_to #order
else
render :edit
end
end
private
def set_order
#order = Order.find(params[:id])
end
def set_booking
#booking = Booking.find(params[:id])
end
def booking_params
params.require(:booking)
.permit(:product_id)
end
end
Another alternative is to use accepts_nested_attributes - but try to keep it simple.
I have recently been advised that for my current rails app relationships I should use the gem nested set. ( My previous thread / question here) I currently have 3 models,
Categories has_many Subcategories
Subcategories belongs_to Categories, and has_many products.
Product belongs_to Subcategory. I wanted to display it something like this
+Category
----Subcategory
--------Product
--------Product
----Subcategory
--------Product
--------Product
+Category
----Subcategory
--------Product
--------Product
So if I were to do this in nested_set, how would I set this up in my Models? Would I remove my subcategory and product models, and just add acts_as_nested_set in the Category model? and once I have the model taken care of, what will I update my controllers actions with, to be able to create nodes in the nested set I create?
I guess just help me understand how I can do the CRUD, create, read, update, and destroying of this nested_set list.
Here is some code I have already
Categories Controller:
class CategoriesController < ApplicationController
def new
#category = Category.new
#count = Category.count
end
def create
#category = Category.new(params[:category])
if #category.save
redirect_to products_path, :notice => "Category created! Woo Hoo!"
else
render "new"
end
end
def edit
#category = Category.find(params[:id])
end
def destroy
#category = Category.find(params[:id])
#category.destroy
flash[:notice] = "Category has been obliterated!"
redirect_to products_path
end
def update
#category = Category.find(params[:id])
if #category.update_attributes(params[:category])
flash[:notice] = "Changed it for ya!"
redirect_to products_path
else
flash[:alert] = "Category has not been updated."
render :action => "edit"
end
end
def show
#category = Category.find(params[:id])
end
def index
#categories = Category.all
end
end
Category Model:
class Category < ActiveRecord::Base
acts_as_nested_set
has_many :subcategories
validates_uniqueness_of :position
scope :position, order("position asc")
end
Subcategory Model:
class Subcategory < ActiveRecord::Base
belongs_to :category
has_many :products
scope :position, order("position asc")
end
And finally, Product Model:
class Product < ActiveRecord::Base
belongs_to :subcategory
has_many :products
scope :position, order("position asc")
end
Any help would be very appreciated.
I would go with a Category and a Product like so:
class Product > ActiveRecord::Base
belongs_to :category
end
class Category > ActiveRecord::Base
has_many :products
acts_as_nested_set
end
class CategoryController < ApplicationController
def create
#category = params[:id] ? Category.find(params[:id]).children.new(params[:category]) : Category.new(params[:category])
if #category.save
redirect_to products_path, :notice => "Category created! Woo Hoo!"
else
render "new"
end
end
def new
#category = params[:id] ? Category.find(params[:id]).children.new : Category.new
end
def index
#categories = params[:id] ? Category.find(params[:id]).children : Category.all
end
end
#config/routes.rb your categories resource could be something like..
resources :categories do
resources :children, :controller => :categories,
:only => [:index, :new, :create]
end
this way is the most flexible, as you can put your products in any a category at any level.