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
Related
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.
I have a model StudentProductRelationship. I am adding a custom validator
validate :validate_primary_product , :if => "!primary_product"
The method is
def validate_primary_tag
unless StudentProductRelationship.exists?(:primary_product => true, :student_id => student_id)
errors.add(:base,"There is no primary product associated to product")
else
end
end
primary_product is a boolean field. I want to validate presence of at least one true primary_product for student_id. The problem is if I have an StudentProductRelationship object say spr with primary_product = true. If I do spr.update_attributes(primary_product: false). The validation does not raise an error because StudentProductRelationship.exists?(:primary_product => true, :student_id => student_id) exists beacuse spr still exists in db with primary_product = true. How do i surpass this?
Doesn't validates_presence_of :primary_product, scope: :student_id work for your?
Currently, my model and validation is this:
class Suya < ActiveRecord::Base
belongs_to :vendor
validates :meat, presence: true
validates_inclusion_of :spicy, :in => [true, false]
end
The problem is that when I run this test:
test "suya is invalid if spiciness is not a boolean" do
suya = Suya.new(meat: "beef", spicy: 1)
suya1 = Suya.new(meat: "beef", spicy: "some string")
assert suya.invalid?
refute suya1.valid?
end
I get a deprecation warning that says:
DEPRECATION WARNING: You attempted to assign a value which is not
explicitly true or false to a boolean column. Currently this value
casts to false. This will change to match Ruby's semantics, and will
cast to true in Rails 5.
So I think my validation is not doing what I think it should be doing. I think my validation checks the presence of the column value and if it IS or is converted to true or false. So I think my test fixtures both convert to false and therefore pass the test which I don't want. What can I do?
You can use custom validation like:
validate :check_boolean_field
def check_boolean_field
false unless self.spicy.is_a?(Boolean)
end
Rails performs type casting any time you assign a value to an attribute. This is a convenience thing. It's not really your text case's fault, it's just how Rails works. If the attribute is a Boolean it'll convert truthy-looking values (true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON') to true and anything else to false. For example:
suya.spicy = "asdf"
suya.spicy # => false
# Likewise for other attribute types:
# Assuming Suya has an `id` attribute that is an Integer
suya.id = "asdf"
suya.id # => 0 # Because "asdf".to_i # => 0
# Assuming Suya has a `name` attribute that is a String
suya.name = 1
suya.name # => "1" # Because 1.to_s # => "1"
So this is just how rails works. In your test case your values are being typecast into their respective attributes' types via mass-assignment.
You can either test out Rails's typecasting by assigning "some value" to your booleans or you can just use more obvious boolean values like true and false in your test cases.
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"
When a controller receives the params of a checked checkbox it comes back as "on" if the box was checked. Now in my case I'm trying to store that value as a boolean, which is typically what you want to with values from checkboxes. My question is, does rails have a way to automatically convert "on" (or even exists) to true/false or do I need to do the following?
value = params[my_checkbox] && params[my_checkbox] == "on" ? true : false
You can just use:
value = !params[:my_checkbox].nil?
as the checkbox would not return any value if not checked (implied by this forum)
The best way of doing this is to create a custom setter for the field in the database, something like this:
class Person < ActiveRecord::Base
def active=(value)
value = value == 'on' ? true : false
super(value)
end
end
That way you don't have to worry about it in the controller and it's the model that knows what value it is supposed to be. When you go to the view rails automatically checks a checkbox from a boolean field. Just in case that didn't work you could also define your own getter.
This can be then used for example in conjunction with store accessor something like this:
class Person < ActiveRecord::Base
validates :active, inclusion: {in: [true, false]}
validates :remember_password, inclusion: {in: [true, false]}
store :settings,
accessors: [:active, :remember_password],
coder: JSON
def active=(value)
value = value == 'on' ? true : false
super(value)
end
def remember_password=(value)
value = value == 'on' ? true : false
super(value)
end
end
Note that the setting field in the database has to be text so you can put more stuff in it.