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.
Related
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
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
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
this is the schema and my model for Visit (visit's status can be: Confirmed, Current, Expired and To be approved)
schema.rb
create_table "visits", force: true do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.date "start"
t.date "end"
t.integer "idVisit"
t.integer "employee_id"
t.integer "visitor_id"
t.string "status", default: "Confirmed"
end
Visit.rb
class Visit < ActiveRecord::Base
belongs_to :employee
belongs_to :visitor
default_scope -> { order(:created_at) }
validates :start, presence: true, uniqueness: {scope: [:end, :visitor_id]}
validates :end, presence: true
validates :visitor_id, presence: true
validates :employee_id, presence: true
validate :valid_date_range_required
def valid_date_range_required
if (start && end) && (end < start)
errors.add(:end, "must be after start")
end
end
end
Now my problem is that I need to compare for each visit, after each time I do show action in employees_controller.rb, the start and end date to Date.today (except for To be approved status); according to it I will change the status of visits in the database.
Here is what I did but probably there will be some mistakes since for now an error occurs at least, so I hope you could help me to fix it.
In Visit.rb I created this:
def check_visit_status(visit)
if visit.status != 'To be confirmed'
if visit.start <= Date.today && visit.end >= Date.today
visit.status = 'Current'
end
if visit.end < Date.today
visit.status = 'Expired'
end
end
end
Now in employees_controller.rb I have (I won't post it all):
class EmployeesController < ApplicationController
after_action :update_status, only: :show
def show
if logged_in?
#employee = Employee.find(params[:id])
#indirizzimac = current_employee.indirizzimacs.new
#visitor = current_employee.visitors.new
#visit = current_employee.visits.new
#visits = current_employee.visits.all
if params[:act]=='myData'
render 'myData'
elsif params[:act]=='myNetwork'
render 'myNetwork'
elsif params[:act]=='temporaryUsers'
render 'temporaryUsers'
elsif params[:act]=='guestsVisits'
render 'guestsVisits'
elsif params[:act]=='myAccount'
render 'myAccount'
else
render 'show'
end
else
render 'static_pages/errorPage'
end
end
def update_status
if #visits.any?
#visits.each do |visit|
check_visit_status(visit)
end
end
end
end
Thank you a lot in advance
I really have to thank eeeeeean for his immense help.
I figured out my problem so I want to post here my solution in order to help someone looking for the same thing I was asking for.
employees_controller.rb
class EmployeesController < ApplicationController
after_action :update_status, only: :show
def show
[...]
end
def update_status
if #visits.any?
#visits.each do |visit|
visit.check_visit_status
end
end
end
end
Visit.rb
def check_visit_status
if self.status != 'To be confirmed'
if self.start <= Date.today && self.end >= Date.today
self.update_attribute :status, 'Current'
end
if self.end < Date.today
self.update_attribute :status, 'Expired'
end
end
end
You need to call check_visit_status on an instance of Visit, but right now it's being called on self, which in this scope refers to the employees controller. Try this:
visit.rb
def check_visit_status
if self.status != 'To be confirmed'
if self.start <= Date.today && end >= Date.today
self.status = 'Current'
end
if self.end < Date.today
self.status = 'Expired'
end
end
end
Then call it like this:
employees_controller.rb
#visits.each do |visit|
visit.check_visit_status
end
That should get you out of that particular error.
I'm making a payroll system for pay salaries. I only need to choice a month, a year and press "create payrolls", and create payrolls for all the teachers
mockup
I created the method "create" in payroll_controller.rb:
Payroll_Manager.new(params[:month], params[:year]).crear_liquidaciones_del_periodo()
The Payroll_Manager is in the file app/helpers/payroll_manager.rb
class Payroll_Manager < PayrollsController
def initialize(month, year)
#month = month
#year = year
end
def crear_liquidaciones_del_periodo
Teachers.each do |t|
t.payrolls.create(#month, #year)
end
end
end
And finally, I have the codel payroll.rb
class Payroll < ActiveRecord::Base
belongs_to :teacher
has_many :payroll_lines
def period
period = month + " " + year
end
validates :month, presence: true
validates :year, presence: true
class Payroll < ActiveRecord::Base
#gross_total, retention_total, neto_total
before_save :calculate_payroll
private
def calculate_payroll
calculate_gross_total
calculate_retention_total
calculate_net_total
end
def calculate_gross_total
self.gross_total = 0
## Concepto.where(type: 1)
haberes = Concepts.all.select{ |c| c.type == 1 }
haberes.each do |h|
parametros_para_linea = {concept_id: h.id, subtotal: h.amount}
self.payroll_line.create(parametros_para_linea)
self.gross_total += h.amount
end
end
def calculate_retention_total
self.retention_total = 0
## Concepto.where(type: 0)
retencion = Concepts.all.select{ |c| c.type == 0 }
retencion.each do |r|
parametros_para_linea = {concept_id: h.id, subtotal: h.amount}
self.payroll_line.create(parametros_para_linea)
self.retention_total += r.amount
end
end
def calculate_net_total
self.net_total = gross_total - retention_total
end
end
end
...When I click the "create payroll" button, I have the error:
uninitialized constant Payroll_Manager::Teachers
enter image description here
Please, help me.
Your class definition in payroll_manager.rb should be PayrollManager, not Payroll_Manager.