2

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.

Radix
  • 2,527
  • 1
  • 19
  • 43
  • Can you try this in your rails console and share the results? – moyinho20 Jun 23 '17 at 08:35
  • @moyinho20 Not sure if you need the results of `rails console`. But here you go, I've updated the question. – Radix Jun 23 '17 at 09:03
  • Could you also check `cart.cart_items.first.quantity`? Maybe `assign_attributes` casts `4abc` to a number. – Wukerplank Jun 23 '17 at 09:28
  • Whats the datatype of quantity specified in schema? – Vamsi Krishna Jun 23 '17 at 09:28
  • @Wukerplank `cart.cart_items.first.quantity` is before and after `assign_attributes` is 4. If old & new integer values are same then, probably, it is casting to number. But if old & new integer values are different then it is not casting to number and shows validation error. – Radix Jun 23 '17 at 09:34
  • @VamsiKrishna, Datatype of `quantity` is `integer`. – Radix Jun 23 '17 at 09:35
  • ```But if old & new integer values are different then it is not casting to number and shows validation error.``` The problem is '4abc'.to_i is 4 and 'abc4'.to_i is 0, But, as you gave ```{ greater_than: 0 }``` you are getting an error. The type casting happens at assign_attributes. Rails does this automatically. Check this https://github.com/rails/rails/blob/4-1-stable/activerecord/lib/active_record/connection_adapters/column.rb#L91-L109 Not sure how to handle this. I remember doing a workaround in my previous project. Will get back to you if I find a solution. – Vamsi Krishna Jun 23 '17 at 10:31
  • If old value is 4 and new value is 5abc, then still it gives validation error. Now, it is not casting 5abc to 5. It seems like `assign_attributes` casts only if integer value of new & old is same. – Radix Jun 23 '17 at 10:39
  • @Mirv "4abc" is just an example. "4abc" means integer + alphanumeric character. I need to show validation error to user if they provide this type of invalid quantity, that's why I'm testing this. – Radix Jun 23 '17 at 13:24
  • https://stackoverflow.com/questions/10700800/how-to-validate-numericality-and-inclusion-while-still-allowing-attribute-to-be – Mirv - Matt Jun 23 '17 at 13:25
  • Possible duplicate of [How to validate numericality and inclusion while still allowing attribute to be nil in some cases?](https://stackoverflow.com/questions/10700800/how-to-validate-numericality-and-inclusion-while-still-allowing-attribute-to-be) – Mirv - Matt Jun 23 '17 at 13:28
  • @Mirv I want to show validation error if user tries to use alphanumeric in the quantity field. Now while updating, if `to_i` of new value is same as the previous value then the no validation error is raised and, if `to_i` of new value differs from old, then it shows the validation error, as expected. For instance, if old value is "4" and new value is "4a" (`to_i` value is same) then the object considered as valid although it should show some error and if new value is "5a" (`to_i` value is different) then it shows validation error as expected. I hope it helps you to understand what I need. – Radix Jun 23 '17 at 13:34
  • @Mirv I've updated the question after checking the answers of [How to validate numericality and inclusion while still allowing attribute to be nil in some cases?](https://stackoverflow.com/questions/10700800/how-to-validate-numericality-and-inclusion-while-still-allowing-attribute-to-be). Please check. – Radix Jun 28 '17 at 05:30
  • @AtulKhanduri ... what if the validations are not being ran on the params - as there were no change in the eyes of the program (via the casting with to_i or whatever you keep talking about). Have you tried setting the quantities param to dirty & see if it validates? – Mirv - Matt Jun 28 '17 at 07:59
  • @Mirv , Not sure what *"setting the quantity param to dirty"* means? Does it mean using [ActiveModel dirty methods](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)? – Radix Jun 28 '17 at 09:19
  • if you open an irb console (no rails involved) and save variable a = "4abc" and then call the "to_i" method on a... ruby gives you 4 which is probably why your validation passes. – engineerDave Jun 28 '17 at 15:08
  • @engineerDave .. Ah... Again same answer/comment. 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 not if `to_i` of new value is same than `quantity` stored in db. – Radix Jun 28 '17 at 16:38
  • @AtulKhanduri ... 3 parts - first, since it's been weeks have you checked issues on the github for these projects & asked there? Second, your post here does not reflect the suggestions I gave you. Lastly, several people have pointed out your edge case is a casting issue (probably in ruby not rails) - you are going to need to read up on resources for this one. – Mirv - Matt Jun 28 '17 at 17:32
  • Three questions: 1) are you absolutely sure that you are not using the `:only_integer => true` option in the validation definition? 2) Precisely what version of ActiveModel gem are you using? You tagged Rails 3 but the raw value is converted to float, not using `to_i`, unless you give the `only_integer` option since at least Rails 3. 3) Can you check if you observe the same behavior without using nested attributes? – Matouš Borák Jun 28 '17 at 19:19
  • @AtulKhanduri you're not getting the error message because ruby's dynamic typing is taking the non-integer part of the string out and just passing the integer part along, i.e dynamic typing is "fixing" it for you, so the validation is always getting an integer – engineerDave Jul 15 '17 at 20:10

6 Answers6

0

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
Md. Farhan Memon
  • 6,055
  • 2
  • 11
  • 36
  • Yes, I was thinking the same way. `quantity_changed?` should have `true` but don't know what's the issue. Also, I don't want to apply the validation in controller. – Radix Jun 28 '17 at 17:08
  • Try `before_save` callback in model instead of `validate` – Md. Farhan Memon Jun 29 '17 at 01:59
  • Thanks Farhan. I've found the solution and posted [above](https://stackoverflow.com/a/44903156/2945616). – Radix Jul 04 '17 at 10:14
  • Although I had already tried `validate` & `_before_type_cast` method. And I don't want to add validation in controller. – Radix Jul 04 '17 at 10:21
0

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.

DonPaulie
  • 2,004
  • 17
  • 26
  • Thanks DonPaulie. That is a known `rails` issue, I've provided details in [above answer](https://stackoverflow.com/a/44903156/2945616). – Radix Jul 04 '17 at 10:17
0

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

itsnikolay
  • 17,415
  • 4
  • 65
  • 64
0

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
FedericoG
  • 197
  • 8
0

This type of issue is already reported in Github.

Reported issues:

Fixed in:

[v4.0]

[v3.2]

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
Radix
  • 2,527
  • 1
  • 19
  • 43
-1
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...

Bharat soni
  • 2,686
  • 18
  • 27