I'm struggling to make my model callback functions behave properly :(
Using Rails 5.2.4.1, Ruby 2.6.3 and pg ~> 0.21
I have a model "Batch" that I want to have automatically calculate and update its own "Value" attribute once its "Price" and "Quantity" values are greater than zero.
def change
create_table :batches do |t|
t.references :product, foreign_key: true, null: false
t.references :currency, foreign_key: true
t.string :batch_number, null: false
t.string :status, null: false, default: "pending"
t.integer :quantity, null: false, default: 0
t.integer :price, null: false, default: 0
t.integer :value, null: false, default: 0
t.timestamps
end
end
end
In my seed file I create some Batch instances with specified Quantity and then Price, and leave the value to default to 0 (this is to be added later when creating an Order instance):
batch1 = Batch.new(
product_id: Product.last.id,
batch_number: "0001-0001-0001",
quantity: 1800
)
if batch1.valid?
batch1.save
p batch1
else
p first_batch1.errors.messages
end
batch1.price = 3
batch1.save
Then my troubles begin...
I've tried a few approaches similar to the below:
after_find :calculate_value
def calculate_value
self.value = price * quantity if value != price * quantity
end
I'm not sure if I'm missing something very obvious here, but the value never seems to update.
I've tried adding save into the method but it doesn't seem to work either. I'm finding some of the other behaviours with saving in these callbacks very strange.
For example, I assign a Currency to a Batch through a join table with this instance method:
after_find :assign_currency
def assign_currency
self.currency_id = currency.id unless currency.nil?
# save
end
If I uncomment that "save" (or make it "self.save") then the seed file creates the Batches but then fails to create the join table, returning {:batch=>["must exist"]}. Yet in the console, the batch does:
[#<Batch:0x00007fb874ad0aa0
id: 1,
product_id: 1,
batch_number: "0001-0001-0001",
status: "pending",
quantity: 1800,
currency_id: nil,
price: 0,
value: 0,
created_at: Thu, 09 Jan 2020 00:38:42 UTC +00:00,
updated_at: Thu, 09 Jan 2020 00:38:42 UTC +00:00>,
I'm still new to rails so would be very grateful for any advice or suggestions whatsoever! This feels like it should be simple and it's driving me crazy...
I'd recommend using a before_validation callback instead of an after_find. The reason I'd recommend that is because in an after_find, the value column will be populated only when the object is loaded using finder (.find, .find_or_create), and hence, the you would not be able to validate the value column before saving. In fact, during initial save, the value column will be empty.
The order of execution of callbacks is as follows:
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
So, in your case, this could work:
before_validation :calculate_value, if: :price_or_quantity_changed?
validates :value, presence: true # This can be added because the before_validation callback will ensure that value is present
def calculate_value
self.value = price * quantity if value != price * quantity
end
private
def price_or_quantity_changed?
self.price_changed? || self.quantity_changed?
end
_changed? methods are from the ActiveModel::Dirty module which helps us keep track of values that have changed in a record.
However, if you would want to use after_find, I think this StackOverflow answer may help you understand it more.
You could use after_find after defining it as a method as follows:
def after_find
<your code to set value>
end
after_initialize & after_find callbacks order in Active Record object life cycle?
how about this in your model?
after_find :calculate_value
def calculate_value
self.value = self.price * self.quantity if self.value != self.price * self.quantity
end
Related
Is there a way to automatically parse string parameters representing dates in Rails? Or, some convention or clever way?
Doing the parsing manually by just doing DateTime.parse(..) in controllers, even if it's in a callback doesn't look very elegant.
There's also another case I'm unsure how to handle: If a date field in a model is nullable, I would like to return an error if the string I receive is not correct (say: the user submits 201/801/01). This also has to be done in the controller and I don't find a clever way to verify that on the model as a validation.
If you're using ActiveRecord to back your model, your dates and time fields are automatically parsed before being inserted.
# migration
class CreateMydates < ActiveRecord::Migration[5.2]
def change
create_table :mydates do |t|
t.date :birthday
t.timestamps
end
end
end
# irb
irb(main):003:0> m = Mydate.new(birthday: '2018-01-09')
=> #<Mydate id: nil, birthday: "2018-01-09", created_at: nil, updated_at: nil>
irb(main):004:0> m.save
=> true
irb(main):005:0> m.reload
irb(main):006:0> m.birthday
=> Tue, 09 Jan 2018
So it comes down to validating the date format, which you can do manually with a regex, or you can call Date.parse and check for an exception:
class Mydate < ApplicationRecord
validate :check_date_format
def check_date_format
begin
Date.parse(birthday)
rescue => e
errors.add(:birthday, "Bad Date Format")
end
end
end
I'm trying to solve a problem with getting all the values saved to my database. Here's how my application is setup is
before_filter :load_make, only: :create
def create
#record = #make.message.new(my_params)
#record.save
end
def load_make
make_id = params[:message] ? params[:message][:make_id] : params[:make_id]
#make = Car.find_by(custom_make: make_id)
#make ||= Car.find_by(id: make_id).tap
end
def my_params
params.require(:message).permit(:message_type, :message_date, :make_id)
end
My problem is that I want to create another method that does a GET request from a different API that returns a created_at date and also save it on create, similar to:
def lookup_car
Car.car_source.find(id: my_params[:make_id]).created_at
# returns a datetime value
end
I'm looking for advice on what's the best way to put this into the my_params method :message_date and save it along with everything else on the create action?
Schema for model:
create_table "messages", force: :cascade do |t|
t.integer "make_id"
t.string "message", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.string "message_type", limit: 255
t.datetime "message_date"
end
Firstly, you should NOT really change / update / insert to the created_at since Rails does that for you. If anything, I suggest you adding another column.
Secondly, you should NOT do and create / update on a Get request.
Other than that, adding another field to your params is easy as long as you have that column ready. Not sure how your models are structure, but here you can do something along the line like this below. Let's say you have message_date column in your whatever model, you can do this:
def my_params
message_date_param = { message_date: Time.now }
params.require(:message).permit(:message_type, :message_date, :make_id).merge(message_date)
end
I wrote this pretty late at night when I probably wasn't making much sense, but essentially what I was wanting was to update another value at same time save was called on the #record. I solved this by using attributes, which updates but doesn't call save (since I was doing that already below).
For example:
def create
car = Car.car_source.find(id: my_params[:make_id]).created_at
#record.attributes = { message_date: car }
#record.save
end
Also much thanks to all the other comments. I've completely refactored this code to be more rails idiomatic.
I have three fields a start_time:time , end_time:time and total_min:decimal fields. The user inputs the start_time and the end_time which are used to calculate the total minutes spent before saving it to the database (sq-lite). How do i calculate that total_min before posting it ?
class CreateInternets < ActiveRecord::Migration
def change
create_table :internets do |t|
t.time :start_time
t.time :end_time
t.decimal :total_min
t.timestamps
end
end
end
Use before_save callback by ActiveRecord.
before_save :update_total_min
def update_total_min
total_min = end_time - start_time
end
http://guides.rubyonrails.org/active_record_callbacks.html#callbacks-overview
You can use arithmetic with Time's. You can add this line to your Internet#create action:
#internet.total_min = params[:start_time] - params[:end_time] / 60
Or using strong_params:
#internet.total_min = internet_params[:start_time] - internet_params[:end_time] / 60
Make sure to whitelist the attributes in your controller:
private
def internet_params
params.require(:internet).permit(:start_time, :end_time)
end
Getting the following error message in the log
ActionController::ParameterMissing - param is missing or the value is empty: user_evaluation_result:
Within the Drills controller under certain conditions I am trying to insert a row in the user_evaluation_results. The lines of code from the Drills Controller are below. I have the feeling I am missing something obvious
....
user_evaluation_result_req = Rails.cache.read("user_evaluation_result_req")
if user_evaluation_result_req
id = Rails.cache.read("evaluation_assumption_id")
#user_evaluation_results = UserEvaluationResult.get_user_evaluation_results(id)
#result_list.each do |result|
if #user_evaluation_results.present?
puts "user evaluation result present "
else
#user_evaluation_result = UserEvaluationResult.new
#user_evaluation_result.evaluation_assumption_id = id
#user_evaluation_result.company_listing_id = 124
#user_evaluation_result.target_share_price_dollars = 7333
#user_evaluation_result = userEvaluationResult.new(user_evaluation_result_params)
end
end
....
# further down in Drills Controller
def user_evaluation_result_params
params.require(:user_evaluation_result).
permit(:evaluation_assumption_id,
:company_listing_id,
:target_share_price_dollars )
end
The value for user_evaluation_results currently hard coded but that changes when I get it working.
The model for user_evaluation_result
class UserEvaluationResult < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
before_validation :clean_data
belongs_to :company_listing
belongs_to :evaluation_assumption
validates :evaluation_assumption_id, :company_listing_id,
:target_share_price_dollars, presence: true
validates :target_share_price_dollars, :numericality => { :only_integer => true,
:greater_than_or_equal_to => 0}
validates :company_listing_id, uniqueness: {scope: [:evaluation_assumption_id]}
def self.get_user_evaluation_results(id)
where("evaluation_assumption_id = ?", id)
end
def target_share_price_dollars
target_share_price.to_d/1000 if target_share_price
end
def target_share_price_dollars=(dollars)
self.target_share_price = dollars.to_d*1000 if dollars.present?
end
private
def clean_data
# trim whitespace from beginning and end of string attributes
attribute_names.each do |name|
if send(name).respond_to?(:strip)
send("#{name}=", send(name).strip)
end
end
end
end
table for user_evaluation_result
create_table "user_evaluation_results", force: true do |t|
t.integer "company_listing_id"
t.integer "target_share_price"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "evaluation_assumption_id"
end
any help welcome and of course can post further details
thanks Pierre
Your UserEvaluationResult called here: #user_evaluation_result = userEvaluationResult.new(user_evaluation_result_params) is lowercase and must be capital so Rails knows you are referring to the User Evaluation Result class. You are getting that error because the params you defined require a UserEvaluationResult to be acting on yet you are not providing one.
I have the following rails model:
class Product < ActiveRecord::Base
end
class CreateProducts < ActiveRecord::Migration
def self.up
create_table :products do |t|
t.decimal :price
t.timestamps
end
end
def self.down
drop_table :products
end
end
But when I do the following in the rails console:
ruby-1.9.2-p180 :001 > product = Product.new
=> #<Product id: nil, price: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :002 > product.price = 'a'
=> "a"
ruby-1.9.2-p180 :003 > product.save
=> true
ruby-1.9.2-p180 :004 > p product
#<Product id: 2, price: #<BigDecimal:39959f0,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
=> #<Product id: 2, price: #<BigDecimal:3994ca8,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
As you can see, I wrote 'a' and it saved 0.0 in the database. Why is that? This is particularly annoying because it bypasses my validations e.g.:
class Product < ActiveRecord::Base
validates :price, :format => /\d\.\d/
end
anything that is invalid gets cast to 0.0 if you call to_f on it
"a".to_f #=> 0.0
you would need to check it with validations in the model
validates_numericality_of :price # at least in rails 2 i think
i dont know what validating by format does, so i cant help you there, but try to validate that it is a number, RegExs are only checked against strings, so if the database is a number field it might be messing up
:format is for stuff like email addresses, logins, names, etc to check for illegeal characters and such
You need to re-look at what is your real issue is. It is a feature of Rails that a string is auto-magically converted into either the appropriate decimal value or into 0.0 otherwise.
What's happening
1) You can store anything into an ActiveRecord field. It is then converted into the appropriate type for database.
>> product.price = "a"
=> "a"
>> product.price
=> #<BigDecimal:b63f3188,'0.0',4(4)>
>> product.price.to_s
=> "0.0"
2) You should use the correct validation to make sure that only valid data is stored. Is there anything wrong with storing the value 0? If not, then you don't need a validation.
3) You don't have to validate that a number will be stored in the database. Since you declared the db field to be a decimal field, it will ONLY hold decimals (or null if you let the field have null values).
4) Your validation was a string-oriented validation. So the validation regexp changed the 0.0 BigDecimal into "0.0" and it passed your validation. Why do you think that your validation was bypassed?
5) Why, exactly, are you worried about other programmers storing strings into your price field?
Are you trying to avoid products being set to zero price by mistake? There are a couple of ways around that. You could check the value as it comes in (before it is converted to a decimal) to see if its format is right. See AR Section "Overwriting default accessors"
But I think that would be messy and error prone. You'd have to set the record's Error obj from a Setter, or use a flag. And simple class checking wouldn't work, remember that form data always comes in as a string.
Recommended Instead, make the user confirm that they meant to set the price to 0 for the product by using an additional AR-only field (a field that is not stored in the dbms).
Eg
attr_accessor :confirm_zero_price
# Validate that when the record is created, the price
# is either > 0 or (price is <= 0 && confirm_zero_price)
validates_numericality_of :price, :greater_than => 0,
:unless => Proc.new { |s| s.confirm_zero_price},
:on => :create
Notes The above is the sort of thing that is VERY important to include in your tests.
Also I've had similar situations in the past. As a result of my experiences, I now record, in the database, the name of the person who said that the value should indeed be $0 (or negative) and let them have a 255 char reason field for their justification. Saves a lot of time later on when people are wondering what was the reason.