Rails 3.2.13 vs Rails 4.0.1 - changed? method changed? - ruby-on-rails

I have recently noticed that the method changed? on ActiveRecord objects has changed between Rails 3.2.13 and Rails 4.0.1. The problem is with fields connected to integer fields in database. Let's assume I have model Model with number integer field:
# Rails 3.2.13
m = Model.last
m.number #=> 5
m.number = '5hello'
m.number #=> 5
m.number_changed? #=> true
m.changed? #=> true
m.changes #=> {:number => [5,5]}
# Rails 4.0.1
m = Model.last
m.number #=> 5
m.number = '5hello'
m.number #=> 5
m.number_changed? #=> false
m.changed? #=> false
m.changes #=> {}
This causes a number of extremely annoying issues with form validations - if a user is trying to change the integer value to have invalid characters (but the type cast will result in the same value as initial one), rails will not invoke save method and none of the validation (including numericality: { only_integer: true }) will be run.
I have managed to get around this problem by overriding number_changed? method to super || number.to_s != number_before_type_cast, however this is extremely ugly.
The question is: why was this changed? Is it a bug or is intentional change? How can it be fixed without overriding all meta methods for integer columns?

I'm not sure on how you're running your validations, but I have a model on my rails app which is called ExtraField and have the following validation:
class ExtraField < ActiveRecord::Base
# stuff
validates :display_order, numericality: { only_integer: true }
# more stuff
end
I'm using rails 4.0.5 and I can do the following:
e = ExtraField.first
e.display_order #=> 1
e.valid? #=> true
e.errors.messages #=> {}
e.display_order = '1banana'
e.display_order #=> 1
e.display_order_changed? #=> false
e.changed? #=> false
e.valid? #=> false
e.errors.messages #=> {:display_order=>["is not a number"]}
So, although the record indeed isn't marked as changed ( what seems correct IMHO ), I can still run my validations and check that the model is not valid. Doesn't look like a bug to me, but just an intentional improvement.
If your form is only validating if the model is responding true to changed? maybe you should check your controller code. Or if you use a gem to help building the form it could be a bug in that gem I guess.

Related

How to get ActiveRecord non-persistant variable to save?

I'm trying to setup an attribute that isn't saved to the database but I can't work out how to change it and read the new value.
class User < ApplicationRecord
attribute :online, :boolean, default: false
end
in Rails Console:
User.first.online = true
=> true
User.first.online
=> false
I'm running Ruby-on-rails 5.2.4.1 and ruby 2.4.1
https://api.rubyonrails.org/v5.2.4.1/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
The line:
User.first
Creates an instance for the first user each time you call it.
User.first.equal?(User.first) #=> false
# ^^^^^^
# Equality — At the Object level, returns true only if obj
# and other are the same object.
You're setting the online attribute of a different instance than the one you're reading from (although they represent the same record). Store the user in a variable. That way you're working with the same instance for both the set and get call.
user = User.first
user.online = true
user.online #=> true

Validation is not working if integer value of old attribute is same as that of new attribute

I've a model Cart having has_many relationship with cart_items.
# cart.rb:
accepts_nested_attributes_for :cart_items, allow_destroy: true
has_many :cart_items, dependent: :destroy, inverse_of: :cart
# cart_item.rb:
validates :quantity, presence: true, numericality: { greater_than: 0 }
# Controller code:
def update
current_cart.assign_attributes(params[:cart])
.....
current_cart.valid?
end
While updating cart_items, if integer (to_i) value of quantity is same as old value then validation is not working.
For example,
If old value of quantity is 4, now new value is updated to 4abc then quantity validation is not working and record is considered as valid.
Although, if new value is updated from 4 to 5abc then it shows a validation error, as expected.
Any suggestions why this all is happening?
EDIT 1:
Here's the output of rails console:
[3] pry(#<Shopping::CartsController>)> cart
=> #<Cart id: 12, created_at: "2017-06-22 13:52:59", updated_at: "2017-06-23 08:54:27">
[4] pry(#<Shopping::CartsController>)> cart.cart_items
[#<CartItem id: 34201, cart_id: 12, quantity: 4, created_at: "2017-06-23 05:25:39", updated_at: "2017-06-23 08:54:27">]
[5] pry(#<Shopping::CartsController>)> param_hash
=> {"cart_items_attributes"=>{"0"=>{"id"=>"34201", "quantity"=>"4abc"}}}
[6] pry(#<Shopping::CartsController>)> cart.assign_attributes param_hash
=> nil
[7] pry(#<Shopping::CartsController>)> cart.valid?
=> true
Here, previous quantity of cart_item is 4 and new value is 4abc, but still the cart is validated.
EDIT 2:
I've checked answers of How to validate numericality and inclusion while still allowing attribute to be nil in some cases?, as it is masked as duplicate in comments, but it does not seems to work.
As I mentioned above, validation is working fine if to_i of new quantity is different than previous quantity, but if it is same then validation is not working.
Also, I tried to apply a method for custom validation using validate, but I'm getting to_i value in that method. Something like:
Model code:
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => 4abc
def validate_quantity
puts quantity_changed? # => false
puts quantity # => 4
end
EDIT 3:
It seems like if to_i of new value is same as previous value then model is casting the value to integer and considering the field not even updated.
EDIT 4:
I'm getting answers & comments about the pupose of to_i. I know what to_i does.
I just want to know why I'm not getting the validation error if to_i of new quantity is similar to the quantity stored in the database.
I know quantity column is integer and ActiveRecord will cast it to integer BUT there must be a validation error as I've added that in model.
I'm getting the validation error if to_i of new value is different than quantity stored in db.
BUT
I'm not getting the validation error if to_i of new value is same than quantity stored in db.
First to answer your question why:
Problably in history it worked differently than today. Personally I suspect this commit (after a very brief search): https://github.com/rails/rails/commit/7500daec69499e4f2da2fc06cd816c754cf59504
And how to fix that?
Upgrade your rails gem... I can recommend Rails 5.0.3, which I tested and it works as expected.
Following the source code for numeric validation, it casts Integer type on the string
"4abc".to_i
=> 4
Hence the input is greater then 0
To resolve it, try to use <input type="numeric" /> in your view to force a user to type only integer values
You should use quantity_before_type_cast, as explained here, in section "Accessing attributes before they have been typecasted". For example:
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => 4abc
def validate_quantity
q = quantity_before_type_cast
return if q == q.to_i
errors.add(:quantity, 'invalid quantity') unless (q.to_i.to_s == q)
end
Since the quantity column is defined as integer, activerecord will typecast it with .to_i before assigning it to the attribute, however, quantity_changed? will be true in that case unlike what you have shown above, so that gives you a hint for solution 1, else you can check if param contains only integers or not in your controller as in solution 2.
Solution 1
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => '4abc'
def validate_quantity
if quantity_changed?
if quantity_was == quantity || quantity <= 0
errors.add(:base, 'invalid quantity')
return false
else
return true
end
else
true
end
end
Solution 2
In your controller,
before_action :validate_cart_params, only: [:create, :update]
private
def validate_cart_params
unless cart_params[:quantity].scan(/\D/).empty?
render json: { errors: "Oops! Quantity should be numeric and greater than 0." }, status: 422 and return
end
end
UPDATE
I googled a bit, but late and found a helper _before_type_cast, it is also a good solution and #Federico has put it as a solution. I am also adding it to my list.
Solution 3
Using quantity_before_type_cast
validate :validate_quantity
# quantity saved in db => 4
# new quantity in form => '4abc'
def validate_quantity
if quantity_changed? || ((actual_quantity = quantity_before_type_cast) != quantity_was)
if actual_quantity.scan(/\D/).present? || quantity <= 0
errors.add(:base, 'invalid quantity')
return false
else
return true
end
else
true
end
end
This type of issue is already reported in Github.
Reported issues:
Parent model not marked as invalid when associated record is invalid, but not dirty
Validations not triggered when attribute is changed from 0 to a string if it's a nested attribute
Validates_numericality_of skipped when changing 0 to "foo" through accepts_nested_attributes_for
Fixed in:
[v4.0]
https://github.com/rails/rails/commit/b9ec47da00e7f5e159ff130441585c3b42e41849
[v3.2]
https://github.com/rails/rails/commit/40617c7e539b8b70469671619e3c1716edcfbf59
All the issues are reported where previous value of an integer attribute is 0 and new value is some string (whose to_i will be 0). Thus the fix is also only for 0.
You can check the same in #changes_from_zero_to_string? in active_record/attribute_methods/dirty.rb, which is initially called from #_field_changed?
Solution:
Override #_field_changed? specifically for quantity field only (due to lack of time. In future, I'll override method for all integer fields).
Now if to_i value of some alphanumeric quantity is equal to current quantity value in database then below method will return true and will not type cast the value.
def _field_changed?(attr, old, value)
if attr == 'quantity' && old == value.to_i && value != value.to_i.to_s
return true
end
super
end
to_i(p1 = v1) public
Returns the result of interpreting leading characters in str as an integer base (between 2 and 36). Extraneous characters past the end of a valid number are ignored. If there is not a valid number at the start of str, 0 is returned. This method never raises an exception when the base is valid.
"12345".to_i #=> 12345
"99 red balloons".to_i #=> 99
"0a".to_i #=> 0
"0a".to_i(16) #=> 10
"hello".to_i #=> 0
"1100101".to_i(2) #=> 101
"1100101".to_i(8) #=> 294977
"1100101".to_i(10) #=> 1100101
"1100101".to_i(16) #=> 17826049
The problem happens because
"4".to_i => 4
"4abc".to_i => 4
That means your custom validation will pass and will not cause any error on the page.
Hope that helps you...

Rails 4 to 5 AR boolean deprecation

I have a model wish contains the bellow method called by before_validation :
def set_to_false
self.confirme ||= false
self.deny ||= false
self.favoris ||= false
self.code_valid ||= false
end
When I run my tests, I got the deprecation message
DEPRECATION WARNING: You attempted to assign a value which is not
explicitly true or false to a boolean column. Currently this value
casts to false. This will change to match Ruby's semantics, and will
cast to true in Rails 5. If you would like to maintain the current
behavior, you should explicitly handle the values you would like cast
to false. (called from cast_value at
./Ruby2.1.0/lib/ruby/gems/2.1.0/gems/activerecord-4.2.1/lib/active_record/type/boolean.rb:17)
I understand I have to cast but I couldn't find a simple and smart way to do it. Any help to remove this deprecation would be great.
Here's a simple booleanification trick that I use often, double negation:
before_validation :booleanify
def booleanify
self.confirm = !!confirm
self.deny = !!deny
...
end
In case you are not familiar with this trick, it'll convert all values to their boolean equivalents, according to ruby rules (nil and false become false, everything else becomes true)
'foo' # => "foo"
!'foo' # => false
!!'foo' # => true
!nil # => true
!!nil # => false

Ruby / Big Decimal - Unable to perform a save

I have a column which needs to hold 18 digits of value. Its defined in my rails file as
Schema.rb
t.decimal "revenue", precision: 23, scale: 5
Now when I try the following in console:
obj = Model.last
obj.revenue = 999999999999999999 ( 18 nines)
obj.save is returning false
( I have a validation that the maximum revenue should be less than 1000000000000000000 ( 1 followed by 18 zeros)
Validation on modal:
validates :revenue , numericality: {:greater_than => WBConstants::MIN_REVENUE_LIMIT, :less_than => WBConstants::MAX_REVENUE_LIMIT}, allow_blank: true
The error message says the value should be less than 100...(1 followed by 18 zeros)
I am unable to save.
The database I am using is sqlserver
The column data type is decmial(23,5)
This is a bug in Rails and there's already a pull request.
You could perform custom validation until the fix is merged.
What's causing this bug?
Rails internally converts the value to a float using Kernel.Float:
f = Kernel.Float(999_999_999_999_999_999)
#=> 1.0e+18
f < 1_000_000_000_000_000_000
#=> false
f == 1_000_000_000_000_000_000
#=> true
Why is this happening?
Because floats have limited precision and can only represent some integers exactly:
(999999999999999000..1000000000000001000).map { |i| i.to_f.to_i }.uniq
#=> [999999999999998976,
# 999999999999999104,
# 999999999999999232,
# 999999999999999360,
# 999999999999999488,
# 999999999999999616,
# 999999999999999744,
# 999999999999999872,
# 1000000000000000000,
# 1000000000000000128,
# 1000000000000000256,
# 1000000000000000384,
# 1000000000000000512,
# 1000000000000000640,
# 1000000000000000768,
# 1000000000000000896,
# 1000000000000001024]
See What Every Computer Scientist Should Know About Floating-Point Arithmetic for details.
You can use custom validation to solve this issue until now.
please check link below:
Custom Validators
Hope this help you.

Validate boolean fields in Rails ActiveRecord [duplicate]

This question already has answers here:
Rails: Validating inclusion of a boolean fails tests
(2 answers)
Closed 8 years ago.
I need to validate boolean fields for a Rails ActiveRecord object. I used the common solution to this problem:
validates :boolean_field, :inclusion => { :in => [true, false] }
But although this solutions solves the problem with validating false values, it still allows to save strings to the boolean_field:
my_object.boolean_field = "string"
my_object.save # => true
my_object.boolean_field # => false
Why does this happen if I specified the inclusion to [true, false]? And more importantly, how do I validate only true and false values?
P.S. If it is important to the question, I'm using PostgreSQL.
You can validate by checking datatype, you can use is_a? and pass class name of the datatype as a parameter
pry(main)> a = "aaaaa"
=> "aaaaa"
pry(main)> a.is_a?(Boolean)
=> false
Since it was a string it returned false, now try for boolean
pry(main)> b = true
=> true
pry(main)> b.is_a?(Boolean)
=> true
Add a custom validation by using this logic
validate :check_boolean_field
def check_boolean_field
errors.add(...) unless boolean_field.is_a?(Boolean)
end

Resources