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.
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'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...
Let's say you had a user with an age attribute that could not be negative
class User < ActiveRecord::Base
validates :age, numericality: { greater_than: 0 }
end
If you attempt to update attributes to a negative number validation will fail but the instance will still have a negative age value
#<User id: 1, age: 5, created_at: "2014-11-08 20:14:12", updated_at: "2014-11-08 20:14:12">
user.update_attributes!(:age => -5)
#<User id: 1, age: -5, created_at: "2014-11-08 20:14:12", updated_at: "2014-11-08 20:14:12">
Other than catching ActiveRecord::RecordInvalid and reseting the value yourself is their a way to reset an instance if its validations fail?
thanks!
You can call model.reload if the validation fails. So it will look something like:
if #model.update_attributes(age: params[:age]) # params[:age] = -5 for example
# model is valid and saved, continue...
else # update_attributes return false and will not raise an exception if model is invalid
# model is invalid, reloading...
#model.reload
# if we call #model.age now, it will return previous value
end
Anyway, update_attributes will set attributes even model is becoming invalid after that update, although it will not persist invalid attributes to database. But remember it will reset all other changes which may have been performed inside this call, so update_attributes(name: params[:name], age: params[age]) will reset both name and age even name is valid.
I would say you need a custom validator, e.g:
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.age.to_i > 0
record.errors[:name] << 'Invalid!'
record.age = record.age_was # Rewrite new with old value
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
With ActiveModel::Dirty there's no need to reload.
I have a situation where an attribute can be created through a JSON API. But once it is created, I want to prevent it from ever being updated.
This constraint causes my first solution, which is using attr_accessible, to be insufficient. Is there a nice way to handle this type of situation in rails, or do I have to perform a manual check in the update method?
You can use attr_readonly, this will allow the value to be set on creation, but ignored on update.
Example:
class User < ActiveRecord::Base
attr_accessible :name
attr_readonly :name
end
> User.create(name: "lorem")
> u = User.first
=> #<User id: 1, name: "lorem">
> u.name = "ipsum"
=> "ipsum"
> u.save
=> true
> User.first.name
=> "lorem"
There is not a nice way to do that as far as I know, you have to write a custom filter
before_update :prevent_attributes_update
def prevent_attribute_updates
%w(attr1, attr2).each do |a|
send("#{attr1}=", send("#{attr1}_was")) unless self.send("#{attr1}_was").blank?
end
end
Something strange is happening in Rails 2.3.14. Any ideas why the following happens?
Example
Suppose we have classes like
class Article < ActiveRecord::Base
has_many :prices
end
class Price < ActiveRecord::Base
end
Then in a irb session the following happens.
>> a = Article.first
=> #<Article id: 980190962>
>> a.prices.first.object_id
=> 97498070
>> a.prices.first.object_id
=> 97470500
>> a.prices.first.object_id
=> 97451010
>> a.valid?
=> true
>> a.prices.first.object_id
=> 97374790
>> a.prices.first.object_id
=> 97374790
So at first the object_id changes each time the record is accessed (yes, it's always the same one). Later after a call to #valid? this behavior stops. Instead everything is fine. You get the same object on each call.
Why is this important?
Let's assume you add a validation to Price
class Price < ActiveRecord::Base
validates_presence_of :amount
end
Then you want to change a price of an article.
>> a = Article.first
=> #<Article id: 980190962>
>> p = a.prices.first
=> #<Price id: 280438907, amount: 1.0, article_id: 980190962>
>> p.amount = nil # oops, accidentally we assigned nil
=> nil
>> p.valid?
=> false
>> a.valid?
=> true
What's that? The price is invalid, but the article is not? This shouldn't have happend, because by default the :validate-option of has_many is set to true. This happens because of the changing object_ids
Every new context(request/response) will regenerate the object IDs. As you are running the query a.prices.first.object_id it will hit the query to get the first price every time and as each hit will generate new object IDs.
But in second case you have extracted first price into a instance variable and working on that so no new request/response is going to database hence it is giving same object IDS.