Custom validator gets time object but needs the string - ruby-on-rails

In my Rails 4.1.6 project, I have a database table with a timestamp:
create_table "jobs", force: true do |t|
...
t.timestamp "run_time", limit: 6
...
end
The model includes a custom validation for that field:
class Job < ActiveRecord::Base
...
validates :run_time, iso_time: true
...
end
The custom validator is:
require "time"
class IsoTimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
p [value.class, value] #DEBUG
errors = []
unless Iso_8601.valid?(value)
errors << "is not an ISO-8601 time"
else
if options[:time_zone]
if Iso_8601.has_time_zone?(value) != options[:time_zone]
errors << [
"should",
("not" unless options[:time_zone]),
"have time zone"
].compact.join(' ')
end
end
end
set_errors(record, attribute, errors)
end
private
def set_errors(record, attribute, errors)
unless errors.empty?
if options[:message]
record.errors[attribute] = options[:message]
else
record.errors[attribute] += errors
end
end
end
end
This validator does not work, because Rails does not pass it the
original string value of the attribute. Instead, Rails converts the
string to a time object before calling the validator. If the string
cannot be converted, it passes nil to the validator:
Job.new(run_time: "ABC").save
# [nilClass, nil]
If the string can be converted, it passes a time object to the
validator:
Job.new(run_time: "01/01/2014").save
# [ActiveSupport::TimeWithZone, Wed, 01 Jan 2014 00:00:00 UTC +00:00]
When validating a timestamp attribute, how can a custom validator
get access to the original string value of the attribute?

Did you try run_time_before_type_cast?
In your validator you could use something like record.send "#{attribute}_before_type_cast"

Related

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.
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.

Automatically parsing date/time parameters in rails

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

Custom Validator Test - Contains Errors?

Wrote a custom validator in my rails project and want to write a test for when nil is passed into the record.
The validator code
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value == nil
record.errors[attribute] << "can't be nil"
elsif value <= Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
Possible test
test "future validator rejects nil values" do
#obj.future = nil
assert_includes #obj.errors
end
I'd like to not only write a test that checks that assert_not #obj.valid? but actually demonstrate that the error message is being passed back. If that's asking too much I'll settle for knowing that an error message is coming back but currently my test isn't working.
It is returning
ArgumentError: wrong number of arguments (1 for 2..3)
Update
Getting close to a green test
test "future validator rejects nil values" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages, {future: ["can't be nil"]}
end
Is returning
FutureValidatorTest#test_future_validator_rejects_nil_values [/Users/rwdonnard/WorkSpace/charter-pay-api/test/models/future_validator_test.rb:42]:
Expected {:future=>["can't be nil"]} to include {:future=>["can't be nil"]}.
The issue seems to be the way you test your class and most probably the exception gets raised from assert_includes. assert_includes expects at least 2 arguments with the first being a collection and the second being the object you expect to be included, in your case an array of errors and the error you expect respectively. Also your test is going to fail if you don't populate the #obj.errors collection, something that requires to call #obj.valid?
Your test should look like this:
test "future validator rejects nil values" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages, { future: ["can't be nil"] }
end
This way you make sure that your model is invalid if future.nil? regardless of other validations in place.
I'd also suggest that you don't check for presence in your custom validator since Rails already provide a way for this. You could have your validator look like this:
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value <= Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
and setup validation in your model like this:
class SomeClass < ActiveRecord::Base
validates :future, presence: true, future: true
end
You could also simplify your tests like:
test "is invalid with nil future date" do
#obj.future = nil
#obj.valid?
assert_includes #obj.errors.messages[:future], "can't be nil"
end
test "future validator rejects past dates" do
#obj.future = Date.new(1987, 1, 1)
#obj.valid?
assert_includes #obj.errors.messages[:future], "can't be in the past!"
end

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

Is it possible for rails to save multiple data type in same db column?

I tried to simulate variable_set and variable_get in drupal which use as site-wide variables storage. I tried something like this.
# == Schema Information
# Schema version: 20091212170012
#
# Table name: variables
#
# id :integer not null, primary key
# name :string(255)
# value :text
# created_at :datetime
# updated_at :datetime
#
class Variable < ActiveRecord::Base
serialize :value
validates_uniqueness_of :name
validates_presence_of :name, :value
def self.set(name, value)
v = Variable.new()
v.name = name
v.value = value
v.save
end
def self.get(name)
Variable.find_by_name(name).value
end
end
but it doesn't work.
I've found a way to do this using yaml for storing your values as encoded strings.
Since I'm not storing the "values" on the database, but their converstions to strings, I named the column encoded_value instead of value.
value will be a "decoder-getter" method, transforming the yaml values to their correct types.
class Variable < ActiveRecord::Base
validates_uniqueness_of :name
validates_presence_of :name, :encoded_value #change in name
def self.set(name, value)
v = Variable.find_or_create_by_name(name) #this allows updates. name is set.
v.encoded_value = value.to_yaml #transform into yaml
v.save
end
def self.get(name)
Variable.find_by_name(name).value
end
def value() #new method
return YAML.parse(self.encoded_value).transform
end
end
This should return integers, dates, datetimes, etc correctly (not only raw strings). In addition, it should support Arrays and Hashes, as well as any other instances that correctly define to_yaml.
I have the following in one of my applications :
class Configure < ActiveRecord::Base
def self.get(name)
value = self.find_by_key name
return value.value unless value.nil?
return ''
end
def self.set(name, value)
elem= self.find_by_key name
if elem.nil?
#We add a new element
elem = Configure.new
elem.key = name
elem.value = value
elem.save!
else
#We update the element
elem.update_attribute(:value, value)
end
return elem.value
end
end
Which is apparently what you're looking for.

Resources