Lefty
Lefty

Reputation: 695

Rails Validation numbericality fails on form object

Related/Fixed: Ruby on Rails: Validations on Form Object are not working

I have the below validation..

validates :age, numericality: { greater_than_or_equal_to: 0, 
                                only_integer: true, 
                                :allow_blank => true
                              }

It is not required, if entered needs to be a number. I have noticed that if someone types in a word instead of a number, the field value changes to 0 after submit and passes validation. I would prefer it to be blank or the entered value.

Update:

Still no solution, but here is more information.

rspec test
   it "returns error when age is not a number" do
      params[:age] = "string"
      profile = Registration::Profile.new(user, params)
      expect(profile.valid?).to eql false
      expect(profile.errors[:age]).to include("is not a number")
    end

Failing Rspec Test:

 Registration::Profile Validations when not a number returns error when age is not a number

 Failure/Error: expect(profile.errors[:age]).to include("is not a number")
   expected [] to include "is not a number"
2.6.5 :011 > p=Registration::Profile.new(User.first,{age:"string"})

2.6.5 :013 > p.profile.attributes_before_type_cast["age"]
=> "string" 

2.6.5 :014 > p.age
=> 0

2.6.5 :015 > p.errors[:age]
=> [] 

2.6.5 :016 > p.valid?
=> true 

#Form Object Registration:Profile:

module Registration
  class Profile
    include ActiveModel::Model

    validates :age, numericality: { greater_than_or_equal_to: 0, 
                                    only_integer: true, 
                                    :allow_blank => true
                                   }
    attr_reader :user
    
    delegate :age , :age=, to: :profile
    
    def validate!
      raise ArgumentError, "user cant be nil" if @user.blank?
    end
    
    def persisted?
      false
    end
    
    def user
      @user ||= User.new
    end
    
    def teacher
       @teacher ||= user.build_teacher
     end
      
     def profile
       @profile ||= teacher.build_profile
     end
     
    def submit(params)
      profile.attributes = params.slice(:age)
      if valid?
        profile.save!
        true
      else
        false
      end
    end 
    def self.model_name
      ActiveModel::Name.new(self, nil, "User")
    end
    
    def initialize(user=nil, attributes={})
      validate!
      @user = user
    end
 end
end

#Profile Model:

class Profile < ApplicationRecord
  belongs_to :profileable, polymorphic: true

  strip_commas_fields = %i[age]

  strip_commas_fields.each do |field|
    define_method("#{field}=".intern) do |value|
      value = value.gsub(/[\,]/, "") if value.is_a?(String) # remove ,
      self[field.intern] = value
    end
  end
end

The interesting thing is that if move the validation to the profile model and check p.profile.errors, I see the expected result, but not on my form object. I need to keep my validations on my form object.

Upvotes: 2

Views: 912

Answers (2)

Pascal
Pascal

Reputation: 8656

If the underlying column in the DB is a numeric type, then Rails castes the value. I assume this is done in [ActiveRecord::Type::Integer#cast_value][1]

def cast_value(value)
  value.to_i rescue nil
end

Assuming model is a ActiveRecord model where age is a integer column:

irb(main):008:0> model.age = "something"
=> "something"
irb(main):009:0> model.age
=> 0
irb(main):010:0>

This is because submitting a form will always submit key value pairs, where the keys values are strings. No matter if your DB column is a number, boolean, date, ...

It has nothing to do with the validation itself.

You can access the value before the type cast like so:

irb(main):012:0> model.attributes_before_type_cast["age"]
=> "something"

If your requirements dictate another behaviour you could do something like this:

def age_as_string=(value)
  @age_as_string = value
  self.age = value
end

def age_as_string
  @age_as_string
end

And then use age_as_string in your form (or whatever). You can also add validations for this attribute, e.g.:

validates :age_as_string, format: {with: /\d+/, message: "Only numbers"}

You could also add a custom type:

class StrictIntegerType < ActiveRecord::Type::Integer
  def cast(value)
    return super(value) if value.kind_of?(Numeric)
    return super(value) if value && value.match?(/\d+/)
  end
end

And use it in your ActiveRecord class through the "Attributes API":

attribute :age, :strict_integer

This will keep the age attribute nil if the value you are trying to assign is invalid.

ActiveRecord::Type.register(:strict_integer, StrictIntegerType) [1]: https://github.com/rails/rails/blob/fbe2433be6e052a1acac63c7faf287c52ed3c5ba/activemodel/lib/active_model/type/integer.rb#L34

Upvotes: 5

Prabin Poudel
Prabin Poudel

Reputation: 369

Why don't you add validations in frontend? You can use <input type="number" /> instead of <input type="text" />, which will only accept number from the user. The way I see you explaining the issue, this is a problem to be resolved in the frontend rather than backend.

You can read more about it here: Number Type Input

Please let me know if this doesn't work for you, I will be glad to help you.

Upvotes: 2

Related Questions