Uninitialized Constant in Namespace Controllers - ruby-on-rails

I haven't had noobie routing errors in a while, but now that I'm using a namespace I don't seem to understand how to trace them...
uninitialized constant ShiftedCommerce::BaseItemsController::Item
So what exactly is this saying? There's no Item model associated with the BaseItemsController within my namespace?
I don't know why there's a trailing ::item, I don't think it should be there.
Another reason I think something strange is going on is because if I change ANYTHING in the BaseItemController, the page will render once. Then If I reload the page, the same error comes back up. It's extremely frustrating.
Here is my BaseItemControlller
class ShiftedCommerce::BaseItemsController < ApplicationController
before_action :set_base_item, only: [:show, :edit, :update, :destroy]
# GET /items
# GET /items.json
def index
#base_items = BaseItem.all
#line_item = LineItem.new
#cart = current_cart
#title = "Store"
end
# GET /items/1
# GET /items/1.json
def show
#cart = current_cart
#line_item = LineItem.new
end
# GET /items/new
def new
#base_item = BaseItem.new
end
and my model...
class ShiftedCommerce::BaseItem < ActiveRecord::Base
has_many :line_items, :through => :items
has_many :items
has_many :sizes, :through => :items
has_many :colors, :through => :items
before_destroy :ensure_not_referenced_by_any_line_item
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
validates :title, :uniqueness => true
def one_record_of_variance
self
end
def has_no_variance
end
def is_base_item?
return true if self.class.name == "BaseItem"
end
def is_item?
return true if self.class.name == "Item"
end
private
# ensure that there are no line items referencing this product
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
end
Here are my paths:
App/controllers/shifted_commerce/base_items_controller.rb
App/models/shifted_commerce/models/base_item.rb

Related

Updating a referenced entity through the create form of another

I have 2 models that are linked through a joint table:
class Dailyreport < ApplicationRecord
max_paginates_per 9
belongs_to :owner
has_many :dailyreport_issues
has_many :issues, through: :dailyreport_issues
accepts_nested_attributes_for :issues, allow_destroy: true
end
class Issue < ApplicationRecord
belongs_to :project
belongs_to :owner
has_many :dailyreport_issues
has_many :dailyreports, through: :dailyreport_issues
max_paginates_per 10
before_create { |issue| issue.jiraid = issue.jiraid.upcase }
validates :jiraid, uniqueness: true
validates :jiraid, :project, :owner, :time_forecast, :time_real, presence: true
validates :jiraid, format: { with: /\b[a-zA-Z]{2,6}-[1-9]\d{0,3}\b/, message: 'must follow this format ABCXYZ-9999' }
validates :time_real, numericality: { only_float: true }
validates :time_forecast, numericality: { only_float: true }
end
class DailyreportIssue < ApplicationRecord
belongs_to :dailyreport
belongs_to :issue
end
I use nested forms 'cocoon gem' to generate issues inside the create form of the dailyreport.
I successfully implemented that with these 2 controllers:
class DailyreportsController < ApplicationController
helper DailyreportsHelper
before_action :define_dailyreport, only: [:edit, :show, :update, :destroy]
def index
#dailyreports = Dailyreport.all.order(created_at: :desc).page params[:page]
end
def new
#dailyreport = Dailyreport.new
#dailyreport.issues.build
#issues = Issue.all.order(created_at: :desc)
end
def edit
end
def show
end
def owner_dailyreport
#owner_dailyreport = current_user.owner.dailyreports
end
def create
#dailyreport = Dailyreport.new(dailyreport_params)
#dailyreport.issues.each do |cr_issue|
call_jira_api("https://agenceinspire.atlassian.net/rest/api/3/issue/#{cr_issue.jiraid}")
if #response_output_issues.key?('errors')
flash.alert = "Please check if #{cr_issue.jiraid} exists and is available on JIRA"
no_api_reponse
else
issue_details_from_jira(cr_issue)
issue_time_real_from_jira(cr_issue)
end
if #dailyreport.save!
redirect_to #dailyreport, notice: 'Dailyreport was successfully created.'
else
render :new
end
end
end
def update
if #dailyreport.update(dailyreport_params)
redirect_to #dailyreport, notice: 'Dailyreport was successfully updated.'
else
render :edit
end
end
def destroy
if current_user.admin? || current_user.email == #dailyreport.owner.email
#dailyreport.destroy
else
admin_only_access
end
previous_page
end
private
def dailyreport_params
params.require(:dailyreport).permit(
:comment,
:owner_id,
issues_attributes: [
:jiraid,
:project_id,
:owner_id,
:time_forecast,
:time_real,
:departement,
:retour_test,
:status,
:_destroy
]
)
end
def define_dailyreport
#dailyreport = Dailyreport.find(params[:id])
end
end
class IssuesController < ApplicationController
require 'net/http'
require 'uri'
before_action :define_issue, only: [:show, :edit, :update, :destroy]
before_action :admin_only_access, only: [:destroy, :edit, :update]
def index
#issues = Issue.all.order(created_at: :desc).page params[:page]
end
def search
if params[:search].blank?
redirect_to issues_path and return
else
#parameter = params[:search].downcase
#results = Issue.all.where('lower(jiraid) LIKE :search', search: "%#{#parameter}%").page params[:page]
end
end
def new
#issue = Issue.new
end
def show
call_jira_api("https://agenceinspire.atlassian.net/rest/api/3/issue/#{#issue.jiraid}")
if #response_output_issues.key?('errors')
flash.alert = "Please check if #{#issue.jiraid} exists and is available on JIRA"
no_api_reponse
else
issue_details_from_jira(#issue)
yes_api_response
end
end
def create
#issue = Issue.new(issue_params)
# Check if issue exists on JIRA
unless call_jira_api("https://agenceinspire.atlassian.net/rest/api/3/issue/#{#issue.jiraid}")
flash.alert = "Please check if #{#issue.jiraid} exists and is available on JIRA"
end
# Get issue details from JIRA
issue_details_from_jira(#issue)
issue_time_real_from_jira(#issue)
# Save the issue
if #issue.save
flash.notice = "Issue #{#issue.jiraid} created"
redirect_to issues_path and return
else
flash.alert = "There was a problem saving #{#issue.jiraid}, check if all the fields are filled on the JIRA issue"
end
end
def edit
end
def update
if #issue.update(issue_params)
redirect_to issues_path
else
render :edit, status: :unprocessable_entity
end
end
def destroy
if current_user.admin?
#issue.destroy
else
admin_only_access
end
previous_page
end
private
def issue_params
params.require(:issue).permit(
:jiraid,
:project_id,
:owner_id,
:time_forecast,
:time_real,
:departement,
:retour_test,
:status
)
end
def define_issue
#issue = Issue.find(params[:id])
#issue_owner = Owner.find_by(params[:current_user])
end
end
My routesRails.application.routes.draw do
get '/search', to: 'issues#search'
get '/home/jira', to: 'home#jira'
get '/dailyreports/owner_dailyreport/:id', to: 'dailyreports#owner_dailyreport', :as => 'my_crs'
resources :projects
resources :issues
resources :departements
resources :owners
resources :dailyreports
# Devise routes
devise_scope :user do
get 'users', to: 'devise/sessions#new'
end
devise_for :users
authenticated :user do
root to: 'home#index', as: :authenticated_root
end
root to: redirect('/users/sign_in')
end
I am trying to implement an update or create process:
Check if the JIRAID exists in my DB
If it doesn't just get the data and save the dailyreport.
If it does, I call the API and get its updated details then update it and save the dailyreport.
And here I found some issues with the code I tried.
First when I update the issue then try to save the dailyreport, it throws the validation error (Jiraid exists) because the dailyreport.save is trying to update the issue again.
I also tried this:
def create
#dailyreport = Dailyreport.new(dailyreport_params)
issues_attributes = params[:dailyreport][:issues_attributes]
p("///////////////////////////////////ISSUES_ATTRIBUTES#{issues_attributes}")
issues_attributes.each do |_, issue_attributes|
p("~~~~~~~~~~~~~~~~~~~~~~ISSUE_ATTRIBUTE#{issue_attributes}")
# Call the JIRA API and check for errors
call_jira_api("https://agenceinspire.atlassian.net/rest/api/3/issue/#{issue_attributes["jiraid"]}")
if #response_output_issues.key?('errors')
flash.alert = "Please check if #{issue_attributes["jiraid"]} exists and is available on JIRA"
return
end
# Update the issue attributes with details from the JIRA API
issue_details_from_jira(issue_attributes)
issue_time_real_from_jira(issue_attributes)
p("~~~~~~~~~~~~~~~~~~~~~~JIRA ID IN THE DB: #{issue.jiraid}")
# Check if the issue already exists in the database
issue = Issue.find_by(jiraid: issue_attributes["jiraid"])
if issue
issue_details_from_jira(issue)
issue_time_real_from_jira(issue)
# Update the existing issue
issue.update(
time_forecast: issue.time_forecast,
time_real: issue.time_real,
status: issue.status
)
else
# Build and save a new issue if it doesn't exist
#dailyreport.issues.build(issue_attributes)
end
end
I know I have an issue here:
issue_details_from_jira(issue_attributes)
issue_time_real_from_jira(issue_attributes)
I am going to have to create an object to pass to my methods. But i don't know how.
I couldn't update the issue from the dailyreport controller too, so I tried passing the update method (+ the id) inside the strong params of the dailyreport. That resulted in a ForbiddenAttributes error.
I actually need a lead of how to approach this, not a specific solution. I think that my approach is wrong.
thank you in advance

Rails - Model not saving to db

I'm completing this airbnb clone course (https://code4startup.com/projects/build-airbnb-with-ruby-on-rails-level-1) but have diverted a bit in order to complete my own project; a marketplace for education camps. Therefore I've added an additional model 'Courses'. It now has User>Listing>Course. This Courses model is working in rails console but not saving to my database when I'm running the server. Any suggestions would be appreciated...
Error Message
ActiveRecord::RecordInvalid in CoursesController#create
Validation failed: Listing must exist
Models
class User < ApplicationRecord
has_many :listings
has_many :courses, :through => :listings
end
class Listing < ApplicationRecord
belongs_to :user
has_many :courses
validates :listing_type, presence: true
validates :course_type, presence: true
validates :accommodate, presence: true
end
class Course < ApplicationRecord
belongs_to :listing
validates :curriculum_type, presence: true
validates :course_places, presence: true
end
Course Controller
class CoursesController < ApplicationController
before_action :set_course, except: [:index, :new, :create]
before_action :authenticate_user!, except: [:show]
def index
#courses = current_user.courses
end
def new
#course = current_user.courses.build
end
def create
#course = current_user.courses.build(course_params)
if #course.save!
redirect_to course_listing_path(#course), notice: "Saved..."
else
render :new, notice: "Something went wrong..."
end
end
def show
end
def listing
end
def pricing
end
def description
end
def photo_upload
end
def amenities
end
def location
end
def update
if #course.update(course_params)
flash[:notice] = "Saved..."
else
flash[:notice] = "Something went wrong..."
end
redirect_back(fallback_location: request.referer)
end
private
def set_course
#course = Course.find(params[:id])
end
def course_params
params.require(:course).permit(:name, :curriculum_type, :summary, :address, :course_places, :start_date, :finish_date, :price)
end
end
Routes
Rails.application.routes.draw do
root 'pages#home'
devise_for :users,
path: '',
path_names: {sign_in: 'login', sign_out: 'logout', edit: 'profile', sign_up: 'registration'},
controllers: { omniauth_callbacks: 'omniauth_callbacks', registrations: 'registrations' }
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :users, only: [:show]
resources :listings, except: [:edit] do
member do
get 'listing'
get 'pricing'
get 'description'
get 'photo_upload'
get 'amenities'
get 'location'
end
end
resources :courses, except: [:edit] do
member do
get 'listing'
get 'pricing'
get 'description'
get 'photo_upload'
get 'amenities'
get 'location'
end
end
end
below you can read my comments with the # sign
You are trying to save an object Course that has belongs_to listings so it is expected that it has as course.listing_id the id of an existing Listing
def create
#course = current_user.courses.build(course_params)
# You need to set #course.listing_id to an existing Listing
# You need to find that listing and save it in a variable.
# I am not getting into your logic, because your code is confused and need many adjustments
listing = Listing.find() # include hear your logic to find an existing listing from the db
#course.listing_id = listing.id
if #course.save!
redirect_to course_listing_path(#course), notice: "Saved..."
else
render :new, notice: "Something went wrong..."
end
end

Rails Association Undefined method error

I would like to add "category" function.
I associated article.rb and category.rb.
However, undefined method `categories' for nil:NilClass was present.
I have no idea.If you know any solution, please tell me.
Index.html.erb
<% unless #article.categories.blank? %>
<% #articles.categories.each do |category|%>
<%= link_to category.name,article_path(category_id:category.id)%>
<%end%>
<%end%>
article.rb
class Article < ApplicationRecord
scope :from_category, -> (category_id) { where(id: article_ids = ArticleCategory.where(category_id: category_id).select(:article_id))}
validates :title, presence: true
validates :content, presence: true
mount_uploader :image,ImageUploader
has_many :categories, through: :article_categories
has_many :article_categories, dependent: :destroy
def save_categories(tags)
current_tags = self.categoires.pluck(:name) unless self.categories.nil?
old_tags = current_tags - tags
new_tags = tags - current_tags
old_tags.each do |old_name|
self.categories.delete Category.find_by(name:old_name)
end
new_tags.each do |new_name|
article_category = Category.find_or_create_by(name:new_name)
self.categories << article_category
end
end
end
category.rb
class Category < ApplicationRecord
validates :name,presense: true,length:{maximum:50}
has_many :articles,through: :article_categories
has_many :article_categories,dependent: :destroy
end
article_category.rb
class ArticleCategory < ApplicationRecord
belongs_to :article
belongs_to :category
validates :article_id,presense:true
validates :category_id,presense:true
end
articles_controller.rb
class ArticlesController < ApplicationController
before_action :set_post, only: [:show,:edit,:update]
before_action :authenticate_user!, :except => [:index,:show]
before_action :set_article_tags_to_gon, only: [:edit]
def index
if params[:category_id]
#selected_category = Category.find(params[:category_id])
#articles= Article.from_category(params[:category_id]).page(params[:page])
else
#articles= Article.all.page(params[:page])
end
#articles = Article.page params[:page]
end
def show
end
def new
#article = Article.new
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to articles_path
else
render 'articles/new'
end
end
def edit
#category_list = #article.categories.pluck(:name).join(",")
end
def update
if #article.update(article_params)
redirect_to articles_path
else
redirect_to 'edit'
end
end
private
def article_params
params[:article].permit(:title,:content,:image,:tag_list,:category)
end
def set_post
#article = Article.find(params[:id])
end
error message
You're calling categories on a nil object, in this case #article. Did you mean to call it on #articles?
If not, you will need to define #article in the index action of your controller.

Has many through or Has and belongs to many missing ID

Just trying to create a simple workout log here I'm getting this error when I try and create weights
"Workout ID must exist"
Models
class Workout < ApplicationRecord
belongs_to :user
has_many :exercises
has_many :weights, through: :exercises
accepts_nested_attributes_for :exercises, :allow_destroy => true
accepts_nested_attributes_for :weights, :allow_destroy => true
validates :bodypart, presence: true, length: { maximum: 255 }
end
class Weight < ApplicationRecord
belongs_to :exercise
belongs_to :workout
validates :amount, presence: true, length: { maximum: 255 }
validates :reps, presence: true, length: { maximum: 255 }
end
class Exercise < ApplicationRecord
belongs_to :workout
has_many :weights
accepts_nested_attributes_for :weights
validates :name, presence: true
validates_associated :weights
end
After reading a few things I thought that a has many through would be the option for these associations but now I'm not so sure. When I try and create a weight for exercise I get the exercise ID but can't seem to get the workout ID despite a number of attempts. I'm not sure if its simply a controller issue or if Im missing something bigger here.
My Current Weights Controller
class WeightsController < ApplicationController
before_action :authenticate_user!
def new
#weight = Weight.new
end
def create
#exercise = Exercise.find(params[:exercise_id])
#weight = #exercise.weights.build(weight_params)
if #weight.save
flash[:notice] = "Set saved"
redirect_to exercise_path(#exercise)
else
redirect_to exercise_path(#exercise)
flash[:notice] = "#{#weight.errors.full_messages.to_sentence}"
end
end
def edit
end
def update
end
def destroy
weight = Weight.find(params[:id])
#weight.destroy
redirect_to root_path
end
private
def weight_params
params.require(:weight).permit(:amount, :reps)
end
end
Routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: { omniauth_callbacks: "callbacks" }
root 'articles#index'
get '/demo', to: 'static_pages#demo'
get '/about', to: 'static_pages#about'
get '/test', to: 'static_pages#index'
resources :articles
resources :workouts do
resources :exercises
end
resources :exercises do
resources :weights
end
resources :workouts do
member do
put :is_finished
put :unfinish
end
end
resources :exercises do
member do
put :is_finished
put :unfinish
end
end
resources :books, :only => :index
end
Exercises controller
class ExercisesController < ApplicationController
before_action :authenticate_user!
# before_action :set_exercise, except: [:index, :new, :create]
def new
#exercise = Exercise.new
#exercise.weights.new
end
def index
#workout = Workout.find(params[:workout_id])
#exercise = #workout.exercises
end
def create
#workout = Workout.find(params[:workout_id])
#exercise = #workout.exercises.build(exercise_params)
if #exercise.save
redirect_to workout_exercise_path(#workout, #exercise)
flash[:notice] = "Exercise created, enter weight and reps for your first set"
else
redirect_to #workout
flash[:notice] = "#{#exercise.errors.full_messages.to_sentence}"
end
end
def edit
end
def update
#exercise = Exercise.find(params[:id])
if #exercise.update_attributes(exercise_params)
redirect_to exercise_path(#exercise)
else
flash[:notice] = "Something went wrong"
end
end
def show
#exercise = Exercise.find(params[:id])
#weights = #exercise.weights.order('created_at DESC').paginate(page: params[:page], per_page: 5)
end
I do have an ID column for workout_id set on my Weights model but it always populates NULL whenever I can manage to create an exercise. Also, in the rails console I can find a workout and call #workout.weights and it returns the weights associated with the workout just fine. I just can't get the workout ID to populate in Weight model. Wondering if has and belongs to many would be better OR if my has many through is just set up wrong. I did try "inverse_of" without any luck. Any help would be appreciated.
The answer is quite simple. To add reverse functionality, you have to explicitly state it, through the exercise.
in weight.rb remove belongs_to :workout and add the following
def workout
self.exercise.workout
end
Perhaps you will need to add logic to avoid nilClass errors.
Now you do have #weight.workout through #weight.exercise

Can't add Products back to Products model after deleting it from Cart, Ruby on rails

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.

Resources