Rails Validation numbericality fails on form object - ruby-on-rails

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.

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

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.

Related

activeadmin and dynamic store accessors fails on new resource

I want to generate forms for a resource that has a postgres jsonb column :data, and I want the schema for these forms to be stored in a table in the database. After a lot of research I am 90% there but my method fails in ActiveAdmin forms upon create (not update). Can anyone explain this?
Sorry for the long code snippets. This is a fairly elaborate setup but I think it would be of some interest since if this works one could build arbitrary new schemas dynamically without hard-coding.
I am following along this previous discussion with Rails 6 and ActiveAdmin 2.6.1 and ruby 2.6.5.
I want to store Json Schemas in a table SampleActionSchema that belong_to SampleAction (using the json-schema gem for validation)
class SampleActionSchema < ApplicationRecord
validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
validate :schema_is_json_schema
private
def schema_is_json_schema
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
unless JSON::Validator.validate(metaschema, schema)
errors.add :schema, 'not a compliant json schema'
end
end
end
class SampleAction < ActiveRecord::Base
belongs_to :sample
validate :is_sample_action
validates :name, uniqueness: { case_sensitive: false }
after_initialize :add_field_accessors
before_create :add_field_accessors
before_update :add_field_accessors
def add_store_accessor field_name
singleton_class.class_eval {store_accessor :data, field_name.to_sym}
end
def add_field_accessors
num_fields = schema_properties.try(:keys).try(:count) || 0
schema_properties.keys.each {|field_name| add_store_accessor field_name} if num_fields > 0
end
def schema_properties
schema_arr=SampleActionSchema.where(category: category)
if schema_arr.size>0
sc=schema_arr[0]
if !sc.schema.empty?
props=sc.schema["properties"]
else
props=[]
end
else
[]
end
end
private
def is_sample_action
sa=SampleActionSchema.where(category: category)
errors.add :category, 'not a known sample action' unless (sa.size>0)
errors.add :base, 'incorrect json format' unless (sa.size>0) && JSON::Validator.validate(sa[0].schema, data)
end
end
This all works correctly; For example, for a simple schema called category: "cleave", where :data looks like data: {quality: "good"}, I can create a resource as follows in the rails console:
sa=SampleAction.new(sample_id: 6, name: "test0", data: {}, category: "cleave" )
=> #<SampleAction id: nil, name: "test0", category: "cleave", data: {}, created_at: nil, updated_at: nil, sample_id: 6>
sa.quality = "good" => true
sa.save => true
To make this system work in AA forms, I call the normal path (new or edit)_admix_sample_action_form with params: {category: "cleave"} and then I generate permit_params dynamically:
ActiveAdmin.register SampleAction, namespace: :admix do
permit_params do
prms=[:name, :category, :data, :sample_id, :created_at, :updated_at]
#the first case is creating a new record (gets parameter from admix/sample_actions/new?category="xxx"
#the second case is updating an existing record
#falls back to blank (no extra parameters)
categ = #_params[:category] || (#_params[:sample_action][:category] if #_params[:sample_action]) || nil
cat=SampleActionSchema.where(category: categ)
if cat.size>0 && !cat[0].schema.empty?
cat[0].schema["properties"].each do |key, value|
prms+=[key.to_sym]
end
end
prms
end
form do |f|
f.semantic_errors
new=f.object.new_record?
cat=params[:category] || f.object.category
f.object.category=cat if cat && new
f.object.add_field_accessors if new
sas=SampleActionSchema.where(category: cat)
is_schema=(sas.size>0) && !sas[0].schema.empty?
if session[:active_sample]
f.object.sample_id=session[:active_sample]
end
f.inputs "Sample Action" do
f.input :sample_id
f.input :name
f.input :category
if !is_schema
f.input :data, as: :jsonb
else
f.object.schema_properties.each do |key, value|
f.input key.to_sym, as: :string
end
end
end
f.actions
end
Everything works fine if I am editing an existing resource (as created in the console above). The form is displayed and all the dynamic fields are updated upon submit. But when creating a new resource where e.g. :data is of the form data: {quality: "good"} I get
ActiveModel::UnknownAttributeError in Admix::SampleActionsController#create
unknown attribute 'quality' for SampleAction.
I have tried to both add_accessors in the form and to override the new command to add the accessors after initialize (these should not be needed because the ActiveRecord callback appears to do the job at the right time).
def new
build_resource
resource.add_field_accessors
new!
end
Somehow when the resource is created in the AA controller, it seems impossible to get the accessors stored even though it works fine in the console. Does anyone have a strategy to initialize the resource correctly?
SOLUTION:
I traced what AA was doing to figure out the minimum number of commands needed. It was necessary to add code to build_new_resource to ensure that any new resource AA built had the correct :category field, and once doing so, make the call to dynamically add the store_accessor keys to the newly built instance.
Now users can create their own original schemas and records that use them, without any further programming! I hope others find this useful, I certainly will.
There are a couple ugly solutions here, one is that adding the parameters to the active admin new route call is not expected by AA, but it still works. I guess this parameter could be passed in some other way, but quick and dirty does the job. The other is that I had to have the form generate a session variable to store what kind of schema was used, in order for the post-form-submission build to know, since pressing the "Create Move" button clears the params from the url.
The operations are as follows: for a model called Move with field :data that should be dynamically serialized into fields according to the json schema tables, both
admin/moves/new?category="cleave" and admin/moves/#/edit find the "cleave" schema from the schema table, and correctly create and populate a form with the serialized parameters. And, direct writes to the db
m=Move.new(category: "cleave") ==> true
m.update(name: "t2", quality: "fine") ==> true
work as expected. The schema table is defined as:
require "json-schema"
class SampleActionSchema < ApplicationRecord
validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
validate :schema_is_json_schema
def self.schema_keys(categ)
sas=SampleActionSchema.find_by(category: categ)
schema_keys= sas.nil? ? [] : sas[:schema]["properties"].keys.map{|k| k.to_sym}
end
private
def schema_is_json_schema
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
unless JSON::Validator.validate(metaschema, schema)
errors.add :schema, 'not a compliant json schema'
end
end
end
The Move table that employs this schema is:
class Move < ApplicationRecord
after_initialize :add_field_accessors
def add_field_accessors
if category!=""
keys=SampleActionSchema.schema_keys(category)
keys.each {|k| singleton_class.class_eval{store_accessor :data, k}}
end
end
end
Finally, the working controller:
ActiveAdmin.register Move do
permit_params do
#choice 1 is for new records, choice 2 is for editing existing
categ = #_params[:category] || (#_params[:move][:category] if #_params[:move]) || ""
keys=SampleActionSchema.schema_keys(categ)
prms = [:name, :data] + keys
end
form do |f|
new=f.object.new_record?
f.object.category=params[:category] if new
if new
session[:current_category]=params[:category]
f.object.add_field_accessors
else
session[:current_category] = ""
end
keys=SampleActionSchema.schema_keys(f.object.category)
f.inputs do
f.input :name
f.input :category
keys.each {|k| f.input k}
end
f.actions
end
controller do
def build_new_resource
r=super
r.assign_attributes(category: session[:current_category])
r.add_field_accessors
r
end
end
end

Can some explain this weird behavior of Rails mass-assignment and validation

Note : Before Answering the question understand this does not have to do anything with attr_accessible or attr_protected the RAILS is in question is 3.1.3 and Ruby 1.9.2p290
Without futher due Here my params that I try to mass assign to DriverLicense Model
{"utf8"=>"✓",
"authenticity_token"=>"cKhruPzVvBK63bqCQPFY8xPZb12V5lhLPOpXgjIToJk=",
"driver_license"=>
{"license_number"=>"LICENCE-001",
"license_class"=>"A-CLASS",
"validity_start_date"=>"02/01/2011",
"validity_end_date"=>"08/24/2011",
"issuing_authority"=>"RTO",
"remarks"=>"Remarks .."},
"save_button"=>"Save",
"action"=>"create",
"controller"=>"driver_licenses",
"driver_id"=>"2"}
Here the controller code
#driver_license = #driver.driver_licenses.new(params[:driver_license])
Here the definition of model look like
class DriverLicense < ActiveRecord::Base
acts_as_tenant(:tenant)
validates :driver_id,:license_number,:validity_start_date,:validity_end_date,:presence => true
validate :date_validity ,:if => :is_date_present?
validate :overlapping_validity,:if => :validity_start_date_present?
belongs_to :driver
scope :active_licenses , where('validity_start_date <= ? and validity_end_date >= ?',Date.today,Date.today)
.... ....
.... ....
private
def is_date_present?
validity_start_date.present? and validity_end_date.present?
end
def date_validity
errors.add(:validity_start_date,"must be earlier then validity end date") if validity_start_date > validity_end_date
end
def validity_start_date_present?
validity_start_date.present?
end
def overlapping_validity
arel = self.class.where('validity_end_date >= ?',validity_start_date).where('driver_id = ?',driver_id)
arel = arel.where('id != ?',id) if id.present?
unless arel.count.zero?
errors.add(:validity_start_date,"overLapping date with other licenses")
end
end
end
The mass-attributes work for all attributes except validity_end_date
#<DriverLicense id: nil, driver_id: 2, license_number: "LICENCE-001", license_class: "A-CLASS", issuing_authority: "RTO", validity_start_date: "2011-01-02", validity_end_date: nil, remarks: "Remarks ..", tenant_id: 2, created_at: nil, updated_at: nil>
Once can check that on screenshot as well further where the mass-assignment work for all attributes except validity_end_date
Found that the date format is not the same in the both the side
& hence it setting it to nil

How do I abandon validations after single (important) failure?

I have a class with some validations:
class HeyThere < ActiveRecord::Base
validate :check_something
validate :check_something_property_1
validate :check_something_property_2
validate :check_something_property_3
def check_something
errors.add(:something, "not there") if something.nil?
end
def check_something_property_1
errors.add(:something, "bad property 1") if something.property_1 > 10
end
def check_something_property_2
errors.add(:something, "bad property 2") if something.property_2 == "ha!"
end
def check_something_property_3
errors.add(:something, "bad property 3") if something.property_3
end
end
The problem is, if something doesn't exist, the first validation triggers, but the second one throws an exception:
undefined method `property_1' for nil:NilClass
For the example, I gave general examples for the validations, but in reality, they are fairly complex. I could change each one to if something && something.property_N whatever, but that feels hacky, and makes the code less-readable, plus, its not very DRY when the number of validations becomes larger.
Is there a way to cancel the remaining validations if the first one fails?
Since they depend on each other, these shouldn't be separate validations. Do this:
class PastaRecipe < ActiveRecord::Base
validate :have_ingredients
private
def have_ingredients
# Don't run the remaining validations if any one validation fails.
have_pasta &&
have_sauce &&
have_water
end
def have_pasta
errors.add(:pasta, "need to buy pasta!") unless pasta.purchased?
end
def have_sauce
errors.add(:sauce, "need delicious sauce!") unless sauce.delicious?
end
def have_water
errors.add(:water, "need to boil water!") unless water.boiled?
end
end
Since you don't want to use if-else statements, I suggest using raise to halt the entire validation chain:
class HeyThere < ActiveRecord::Base
validate :check_something
validate :check_something_property_1
validate :check_something_property_2
validate :check_something_property_3
def check_something
if something.nil?
errors.add(:something, "not there")
raise ActiveRecord::RecordInvalid, self
end
end
def check_something_property_1
errors.add(:something, "bad property 1") if something.property_1 > 10
end
def check_something_property_2
errors.add(:something, "bad property 2") if something.property_2 == "ha!"
end
def check_something_property_3
errors.add(:something, "bad property 3") if something.property_3
end
end
I did something like this. I'm just using the validations module here, but it applies to active record models the same way. There are a lot of different modifications that can be done to this, but this is almost the exact implementation I used.
Must list validations in order
Can be modified to halt on any error including instance.errors[:base]
Can halt on ANY error using instance.errors.any?
with_options method will actually pass the "unless: Proc" to each validation, so it actually
runs for each validation
with_options can be replaced by just putting if or unless conditions on each validation
Class definition. Non-AR version. Same can be done with ActiveRecord::Base classes
class Skippy
# wouldnt need to do this initialize on the ActiveRecord::Base model version
include ActiveModel::Validations
validate :first
validate :second
validate :halt_on_third
validates_presence_of :or_halt_on_fourth
with_options unless: Proc.new{ |instance| [:halt_on_thirds_error_key, :or_halt_on_fourth].any?{ |key| instance.errors[key].any? } } do |instance|
instance.validate :wont_run_fifth
instance.validates_presence_of :and_wont_run_sixth
end
# wouldn't need to do these on the ActiveRecord::Base model version
attr_accessor :attributes
def initialize
#attributes = { or_halt_on_fourth: "I'm here" }
end
def read_attribute_for_validation(key)
#attributes[key]
end
def first
errors.add :base, 'Base error from first'
end
def second
errors.add :second, 'Just an error'
end
def halt_on_third
errors.add :halt_on_thirds_error_key, 'Halting error' unless #donthalt
end
def wont_run_fifth
errors.add :wont_run_fifth, 'ran because there were no halting errors'
end
end
Demo
2.0.0 :040 > skippy = Skippy.new
=> #<Skippy:0x0000000d98c1c0 #attributes={:or_halt_on_fourth=>"I'm here"}>
2.0.0 :041 > skippy.errors.any?
=> false
2.0.0 :042 > skippy.valid?
=> false
2.0.0 :043 > skippy.errors.full_messages
=> ["Base error from first", "Second Just an error", "Halt on thirds error key Halting error"]
2.0.0 :044 > skippy.errors.clear
=> {}
2.0.0 :045 > skippy.instance_variable_set(:#donthalt, true)
=> true
2.0.0 :046 > skippy.errors.any?
=> false
2.0.0 :047 > skippy.valid?
=> false
2.0.0 :048 > skippy.errors.full_messages
=> ["Base error from first", "Second Just an error", "Wont run fifth ran because there were no halting errors", "And wont run sixth can't be blank"]
2.0.0 :049 > skippy.errors.clear; skippy.attributes = {}; skippy.errors.any?
=> false
2.0.0 :050 > skippy.valid?; skippy.errors.full_messages
=> ["Base error from first", "Second Just an error", "Or halt on fourth can't be blank"]
2.0.0 :051 >

Datetime field saves nil value when saving a string

I have a problem with Datetime field in my Rails app. I have a validation that should accept valid Datetime, and allows null or blank values (code is from my question yesterday):
include ActiveModel::Validations
class DateValidator < ActiveModel::EachValidator
def validate_each(record,attribute,value)
record.errors[attribute] << "must be a valid datetime" unless ((DateTime.parse(value) rescue nil))
end
end
validates :datetime_field :date => true, :allow_nil => true, :allow_blank => true
However, when set datetime_field to some string, my model overrides the previous value of datetime_field and sets it to nil (in rails console I get the following:
object.update_attributes("datetime_field" => "Now")
true
object.datetime_field.nil?
true
How to stop setting my datetime field to nil after updating with string, and at the same time keep being able to blank this field explicitly?
You validations are strange: datetime_field should be a date and at the same time it can be nil or blank. But nil or blank can't be date. So your validation should sounds like: datetime_field should be DATE or BLANK or NIL:
include ActiveModel::Validations
class DateOrBlankValidator < ActiveModel::EachValidator
def validate_each(record,attribute,value)
record.errors[attribute] << "must be a valid datetime or blank" unless value.blank? or ((DateTime.parse(value) rescue nil))
end
end
validates :datetime_field :date_or_blank => true
UPD
Just for notice. nil.blank? => true. So you never need to validate something if it is nil if you are checking if it is blank?. Blank? will return true for empty objects and for nil objects: "".blank? => true, nil.blank? => true
Is your datetime_field marked as datetime in your migration?
If so datetime_field is being set to nil becouse string you've passed isn't valid datetime string. Try doing this instead:
object.update_attributes("datetime_field" => "2010-01-01")
true
Is suppose then
object.datetime_field
should return
2010-01-01

How do you validate the presence of one field from many

I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""

Resources