rails Calculation (price * quantity) - ruby-on-rails

Hi Im creating an ec site in my rails.
My migration:
(Item) has :name and :price.
(Basket_Item) has :item_id(fk), :basket_id(fk) and :quantity.
The system User will add some items to their basket.
So Basket_items is JOIN Table between (Item) and (Basket)
see like below.
What I want to do:
Get a price of Item and get a quantity from Basket_Items which is selected by user. Then I want to create #total_price = item_price * item_quantity.
Can anyone help me to create the #total_price please.
This is my a try code but it doesn't work on rails console.
Basket_items
class CreateBasketItems < ActiveRecord::Migration[5.2]
def change
create_table :basket_items do |t|
t.references :basket, index: true, null: false, foreign_key: true
t.references :item, index: true, null: false, foreign_key: true
t.integer :quantity, null: false, default: 1
t.timestamps
end
end
end
///
Items
class CreateItems < ActiveRecord::Migration[5.2]
def change
create_table :items do |t|
t.references :admin, index: true, null: false, foreign_key: true
t.string :name, null: false, index: true
t.integer :price, null: false
t.text :message
t.string :category, index: true
t.string :img
t.string :Video_url
t.text :discription
t.timestamps
end
end
end
///
This is my try a code but it doesn't work on rails console.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price)
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity)
def self.total(items_price, items_quantity)
sum(items_price * items_quantity)
end
#total_price = basket.total(items_price, item_quantity)

There are a few issues with your code:
You are trying to call a class method on an instance of the class. That's not gonna work, second you are passing in arrays into the calculation.
basket = current_user.prepare_basket
item_ids = basket.basket_items.select(:item_id)
items = basket.items.where(id: item_ids)
items_price = items.select(:price) # => Array of prices from the items in the basket
items_quantity = basket.basket_items.where(item_id: item_ids).pluck(:quantity) # => Array of quantities from the items in the basket
def self.total(items_price, items_quantity)
sum(items_price * items_quantity) # => So this line will do: sum(['12,95', '9.99'] * [1, 3])
end
#total_price = basket.total(items_price, item_quantity)
As you can see, that ain't gonna work. First you need to change the method and remove the self.
def total(items_price, items_quantity)
# ...
end
Now you can call the total method on a basket object: basket.total(items_price, items_quantity)
And inside the total method you need to loop through each items to do the calculation and add all the results.
def total(items_price, items_quantity)
total_price = 0
items_price.each_with_index do |price, index|
total_price += price * items_quantity[index]
end
total_price
end
But this solution could also fail, because you don't know sure that the order in the items_price is matching with the order of items_quantity. So a better approach would be to do the calculation for each basket_item seperate.
# Basket model
def total
total_price = 0
basket_items.each do |basket_item|
total_price += basket_item.total_price
end
total_price
end
# BasketItem model
def total_price
quantity * item.price
end
Now you can call it like this: basket.total

Related

Same method between model & controller

To practice, I'm trying to create a small web game with Ruby On Rails, however, I need some information in my model and also in my controller.
I explain myself. For example, in my model, I have the following method:
def cost(building)
{
wood: BUILDING[building]['wood'] * (BUILDING[building]['factor'] ** self.level(building)),
stone: BUILDING[building]['stone'] * (BUILDING[building]['factor'] ** self.level(building)),
iron: BUILDING[building]['iron'] * (BUILDING[building]['factor'] ** self.level(building))
}
end
Then in my view I use the following to display the cost of upgrading the building :
<%= #buildings.cost(building[0])[:wood] %>
But I also have an "upgrade" button in my view that allows to upgrade this building, except that on the controller side I also need to get the costs of the building but I'm not sure of the right approach.
[EDIT] :
To give more informations :
#buildings = #buildings = Building.find_by(kingdom_id: current_kingdom.id) #Inside BuildingsController
building[0] It is a string in a yaml that corresponds to the name of a building
level it recovers the current level of the building for the player
EDIT 2 :
models/building.rb :
class Building < ApplicationRecord
def cost(building)
{
wood: BUILDING[building]['wood'] * (BUILDING[building]['factor'] ** self.level(building)),
stone: BUILDING[building]['stone'] * (BUILDING[building]['factor'] ** self.level(building)),
iron: BUILDING[building]['iron'] * (BUILDING[building]['factor'] ** self.level(building))
}
end
def consumption(building)
BUILDING[building]['coal']
end
def time(building)
resources_needed = cost(building)[:wood] + cost(building)[:stone] + cost(building)[:iron]
time = (resources_needed / (2500 * 4 * SERVER['rate']).to_f * 3600).round
if time >= 3600
"#{time / 60 / 60} h #{time % 3600 / 60} min"
elsif time >= 60
"#{time / 60} min #{time % 60 } sec"
else
"#{time} sec"
end
end
def level(building)
self[building.to_sym]
end
def upgrade?(building, kingdom_resources)
cost(building)[:wood] <= kingdom_resources[:wood] &&
cost(building)[:stone] <= kingdom_resources[:stone] &&
cost(building)[:iron] <= kingdom_resources[:iron]
end
end
buildings_controller.rb :
class BuildingsController < ApplicationController
def index
#buildings = Building.find_by(kingdom_id: current_kingdom.id)
#kingdom_resources = kingdom_resources
#kingdom_queue = BuildQueue.where(kingdom_id: current_kingdom.id)
end
def add_to_queue
building = params[:building]
# If we can upgrade
# Add building to the queue
end
private
def cost(building)
{
wood: BUILDING[building]['wood'] * (BUILDING[building]['factor'] ** self.level(building)),
stone: BUILDING[building]['stone'] * (BUILDING[building]['factor'] ** self.level(building)),
iron: BUILDING[building]['iron'] * (BUILDING[building]['factor'] ** self.level(building))
}
end
def building_level(building)
Building.find_by(kingdom_id: current_kingdom.id)[building.to_sym]
end
def time(building)
resources_needed = cost(building)[:wood] + cost(building)[:stone] + cost(building)[:iron]
(resources_needed / (2500 * 4 * SERVER['rate']).to_f * 3600).round
end
def upgrade?(building)
cost(building)[:wood] <= kingdom_resources[:wood] &&
cost(building)[:stone] <= kingdom_resources[:stone] &&
cost(building)[:iron] <= kingdom_resources[:iron]
end
end
method inside app/controllers/application_controller.rb,
To get the current_kingdom :
def current_kingdom
return nil unless current_user
return #_kingdom if #_kingdom
#_kingdom = Kingdom.find_by(user_id: current_user.id)
end
And current_user :
def current_user
return nil if !session[:auth] || !session[:auth]['id']
return #_user if #_user
#_user = User.find_by_id(session[:auth]['id'])
end
And current kingdom_resources :
def kingdom_resources
return #kingdom if #kingdom
#kingdom = {
wood: current_kingdom.wood,
stone: current_kingdom.stone,
iron: current_kingdom.iron,
coal: current_kingdom.coal,
food: current_kingdom.food
}
end
Thank's in advance,
Regards
fA user has a kingdom. A kingdom has buildings. We can set that up simple enough. A kingdom apparently also has build queues and resources.
class User < ApplicationRecord
has_one :kingdom
has_many :buildings, through: :kingdom
end
create_table :kingdoms do |t|
t.belongs_to :user, foreign_key: true
t.name :string, null: false
t.wood :integer, null: false
t.stone :integer, null: false
t.iron :integer, null: false
t.coal :integer, null: false
t.food :integer, null: false
end
class Kingdom < ApplicationRecord
belongs_to :user
has_many :buildings
has_many :build_queues
def resource(material)
self[material]
end
end
class BuildQueue < ApplicationRecord
belongs_to :kingdom
belongs_to :building
end
Now you can directly ask a player for its buildings: current_user.buildings and a kingdom for its build queues: kingdom.build_queues.
If you want to find a building by name: current_user.buildings.find_by(name: building_name).
Your Building model is strange. It seems like a single Building object represents all buildings. And information about the cost of a building is stored in a global.
Instead, the information about each building should instead be stored in a table row.
create_table :buildings do |t|
t.belongs_to :kingdom, foreign_key: true
t.string :name, null: false
t.cost_wood :integer, null: false
t.cost_stone :integer, null: false
t.cost_iron :integer, null: false
t.consumption_coal :integer, null: false
t.cost_factor :float, default: 1, null: false
t.level :integer, default: 1, null: false
end
class Building < ApplicationRecord
belongs_to :kingdom
has_one :player, through :kingdom
MATERIALS = [:wood, :stone, :iron].freeze
private def base_cost(material)
self[:"cost_#{material}"]
end
def cost(material)
base_cost(material) * cost_factor ** level
end
def build_cost
MATERIALS.sum { |m| cost(m) }
end
def build_time
# ActiveSupport::Duration will do the formatting for you.
ActiveSupport::Duration.build(
(build_cost / (2500 * 4 * SERVER['rate']).to_f * 3600).round
)
end
end
Now the BuildingsController can use what's been set up in the models.
class BuildingsController < ApplicationController
def index
# A user already has associations to its kingdom and buildings
# This is so simple there's no need for a current_kingdom in
# the ApplicationController
#kingdom = current_user.kingdom
#buildings = current_user.buildings
end
def show
if params[:id].present?
#building = current_user.buildings.find_by(id: params[:id])
else
#building = current_user.buildings.find_by(name: params[:name])
end
end
end
If you want to take action that involves multiple models, make a little object to do it. Like queuing a building upgrade.
# BuildingUpgradeQueuer.new(
# building: building,
# kingdom: kingdom
# ).queue_upgrade
class BuildingUpgradeQueuer
include ActiveModel::Model
MATERIALS = [:wood, :stone, :iron].freeze
attr_accessor :buiding, :kingdom
def queue_upgrade
return unless upgrade?
kingdom.build_queues.create!(building: building)
end
def upgrade?
MATERIALS.all? { |material|
building.cost(material) <= kingdom.resources(material)
}
end
end
You could do this in the Building or Kingdom object, but if you do that your models get complex and fat.

fetch user_profiles by avoiding N+1 query in graphql

I am using graphiql-ruby gem for using graphql. I am trying to get the list of users with its userprofile and its company. However, it consumes more than 5 minutes and hits the api several times. It even crashes my server. Can anyone help me at this, please?
this is what i have done
module Queries
module Users
extend ActiveSupport::Concern
include GraphQL::Schema::Member::GraphQLTypeNames
included do
field :users, [Types::UserType], null: false
field :user, Types::UserType, null: false do
argument :id, ID, required: true
end
field :currentUser, Types::UserType, null: false
end
# Get all users list except admins
def users
User.where.not(roles: ['admin'])
# context[:current_user].logs.info!('User Data Fetched') && user
end
def user(id:)
User.find(id)
end
def current_user
context[:current_user]
end
end
end
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :email, String, null: false
field :user_profile, Types::UserProfileType, null: false
field :grants, [Types::GrantType], null: true
field :companies, [Types::CompanyType], null: true
field :my_companies_with_grants, [Types::CompanyGrantType], null: true
def grants
Grant.where(user_id: object.id)
end
def my_companies_with_grants
current_user = User.find_by(id: object.id)
company = current_user.companies
userid = current_user.id
company.map {|comp| { company: comp, grants: comp.get_user_grants(userid), company_id: comp.id } }
end
end
end
Here is how i am querying for the list of users
query {
employees: users {
id
email
userProfile {
...UserProfile
}
roles
createdAt
companies {
...Company
}
grants {
...Grant
}
}
}
${COMPANY}
${GRANT}
${USER_PROFILE}
why it is taking too long time to load all the users ? How can it be make efficient?
UPDATE
class ApiSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Batch
end
Record loader
class RecordLoader < GraphQL::Batch::Loader
def initialize(model)
#model = model
end
def perform(ids)
#model.where(id: ids).each { |record| fulfill(record.id, record) }
ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
end
end
I changed my user_type.rb and did the following for user_profile
field :user_profile, [Types::UserProfileType, null: true], null: false do
argument :id, [ID], required: true
end
def user_profile(ids:)
RecordLoader.for(UserProfile).load_many(ids)
end
But i get "message":"Field 'userProfile' is missing required arguments: ids" issue
I setup the graphql-batch but could not figure out where should i use RecordLoader as per the way I am using(types and resolvers)

Why `after_save` not work when saving a copy data with Rails?

Rails version: 5.2.2.1
DB
db/migrate/20190520050333_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
db/migrate/20190520050350_create_post_copies.rb
class CreatePostCopies < ActiveRecord::Migration[5.2]
def change
create_table :post_copies do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
Model
app/models/post.rb
class Post < ApplicationRecord
after_save :save_post_copy
private
def save_post_copy
if title_changed?
post_copy = PostCopy.new
post_copy.id = self.id
post_copy.title = self.title
post_copy.body = self.body
post_copy.save!
end
end
end
Console
post = Post.first
post.title = 'change title'
post.title_changed? # => true
post.save!
PostCopy.first
=> nil
Here want to auto save the record to post_copies when the title been changed in posts. But after the record saved in posts, can't find anything in post_copies.
Probably id should not be explicitly set, as rails automatically assign the value of the id
def save_post_copy
if self.title_changed?
post_copy = PostCopy.new
#post_copy.id = self.id
post_copy.title = self.title
post_copy.body = self.body
post_copy.save!
end
end
Or alternatively
after_save :save_post_copy, if: : saved_change_to_title
def save_post_copy
post_copy = PostCopy.new
post_copy = self.dup
post_copy.save
end

How to calculate total price in def create, API

Can you help me? I dont understand how to do it.
How to calculate TOTAL PRICE when i created the room?
I am dont understand how to do it right.
Here is my code:
room_controller.rb
def create
parameters = room_params.to_hash
parameters[:created_by] = #current_user.id
parameters[:account_id] = #current_user.account_id
#room = #section.rooms.create!(parameters)
update_room(#room, params)
end
...
def update
params = room_params.to_hash
update_room(#room, params)
json_response(#room)
end
def update_room(room, json)
return unless room
unless json.key?('total_price')
if json.key?('price') || json.key?('square')
square = json.key?('square') ? json['square'].to_f : room.square
price = json.key?('price') ? json['price'].to_f : room.price
json['total_price'] = price * square
end
end
unless json.key?('price')
if json.key?('total_price') || json.key?('square')
square = json.key?('square') ? json['square'].to_f : room.square
total_price = json.key?('total_price') ? json['total_price'].to_f : room.total_price
json['price'] = total_price / square
end
end
room.update(json)
end
def room_params
params.permit(
:level, :square, :total_price, :price, :number, :room_type,
:plan_image, :plan_coordinate, :view_image, :interior_image,
:rooms_count, :status, :marked
)
end
schema.rb
create_table "rooms", force: :cascade do |t|
...
t.float "square", default: 0.0
t.float "price", default: 0.0
t.float "total_price", default: 0.0
If total_price is square multiplied by price you can do that in the model.
class Room
before_save :calculate_total
private
def calculate_total
self.total_price = square * price
end
end

Rails Name Error - Undefined local variable or method `booking'

I'm building an Events app in Rails and I've hit the error above which relates to this method in my Model -
def validate_availability
errors.add(:base, 'event is fully booked') if booking.count >= event.number_of_spaces
end
The purpose of the method is to avoid over-booking of an event whereby a specific number of spaces are available. In my Controller I have the following code -
Controller#Create
def create
#event = Event.find(params[:event_id])
#booking = #event.bookings.new(booking_params)
#booking.user = current_user
if
#booking.set_booking
flash[:success] = "Your place on our event has been booked"
redirect_to event_booking_path(#event, #booking)
else
flash[:error] = "Booking unsuccessful"
render "new"
end
if #event.is_free?
#booking.save(booking_params)
end
if booking.count >= #event.number_of_spaces
flash[:error] = "Sorry, this event is now fully booked"
render "new"
end
end
I need to define booking.count in my controller but not sure what would work - tried a few things but nothings working. I have the following in my schema -
create_table "bookings", force: :cascade do |t|
t.integer "event_id"
t.integer "user_id"
t.string "stripe_token"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "quantity", default: 1
t.integer "total_amount"
t.string "stripe_charge_id"
t.string "booking_number"
end
The booking.count would rely upon the quantity of spaces/bookings a user wishes to make versus the number of spaces remaining but how do I express this? Do I need a total_bookings column in my table or a separate method?
UPDATE -
Booking.rb
class Booking < ActiveRecord::Base
belongs_to :event
belongs_to :user
before_create :set_booking_number
validates :quantity, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :total_amount, presence: true, numericality: { greater_than_or_equal_to: 0 }
validate(:validate_booking)
validate(:validate_availability)
def set_booking_number
self.booking_number = "MAMA" + '- ' + SecureRandom.hex(4).upcase
end
def set_booking
if self.event.is_free?
self.total_amount = 0
save!
else
self.total_amount = event.price_pennies * self.quantity
begin
charge = Stripe::Charge.create(
amount: total_amount,
currency: "gbp",
source: stripe_token,
description: "Booking created for amount #{total_amount}")
self.stripe_charge_id = charge.id
save!
rescue Stripe::CardError => e
# if this fails stripe_charge_id will be null, but in case of update we just set it to nil again
self.stripe_charge_id = nil
# we check in validatition if nil
end
end
end
def validate_booking
# stripe_charge_id must be set for not free events
unless self.event.is_free?
return !self.stripe_charge_id.nil?
end
end
private
def validate_availability
errors.add(:base, 'event is fully booked') if event.bookings.count >= event.number_of_spaces
end
end
For the counts of booking table, you should have a booking_count field in events table. Use the counter cache for this. For more details check http://guides.rubyonrails.org/association_basics.html. This is very helpful when records are large.
Your migration for adding column should be similar as below:
def change
add_column :events, :bookings_count, :integer, default: 0
Event.reset_column_information
Event.all.each do |e|
Event.update_counters e.id, :bookings_count => e.bookings.length
end
end

Resources