Does rails do any validation for datetime? I found a plugin
http://github.com/adzap/validates_timeliness/tree/master,
but it seems like something that should come in out of the box.
There's no built-in ActiveRecord validator for DateTimes, but you can easily add this sort of capability to an ActiveRecord model, without using a plugin, with something like this:
class Thing < ActiveRecord::Base
validate :happened_at_is_valid_datetime
def happened_at_is_valid_datetime
errors.add(:happened_at, 'must be a valid datetime') if ((DateTime.parse(happened_at) rescue ArgumentError) == ArgumentError)
end
end
Gabe's answer didn't work for me, so here's what I did to validate my dates:
class MyModel < ActiveRecord::Base
validate :mydate_is_date?
private
def mydate_is_date?
if !mydate.is_a?(Date)
errors.add(:mydate, 'must be a valid date')
end
end
end
I was just looking to validate that the date is in fact a date, and not a string, character, int, float, etc...
More complex date validation can be found here: https://github.com/codegram/date_validator
Recent versions of Rails will type cast values before validation, so invalid values will be passed as nils to custom validators. I'm doing something like this:
# app/validators/date_time_validator.rb
class DateTimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.public_send("#{attribute}_before_type_cast").present? && value.blank?
record.errors.add(attribute, :invalid)
end
end
end
# app/models/something.rb
class Something < ActiveRecord::Base
validates :sold_at, date_time: true
end
# spec/models/something_spec.rb (using factory_girl and RSpec)
describe Something do
subject { build(:something) }
it 'should validate that :sold_at is datetimey' do
is_expected.not_to allow_value(0, '0', 'lorem').for(:sold_at).with_message(:invalid)
is_expected.to allow_value(Time.current.iso8601).for(:sold_at)
end
end
You can create a custom datetime validator by yourself
1) create a folder called validators in inside app directory
2) create a file datetime_validator.rb. with the following content inside app/validators directory
class DatetimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if ((DateTime.parse(value) rescue ArgumentError) == ArgumentError)
record.errors[attribute] << (options[:message] || "must be a valid datetime")
end
end
end
3) Apply this validation on model
class YourModel < ActiveRecord::Base
validates :happend_at, datetime: true
end
4) Add the below line in application.rb
config.autoload_paths += %W["#{config.root}/app/validators/"]
5) Restart your rails application
Note: The above method tested in rails 4
I recommend a gem date_validator. See https://rubygems.org/gems/date_validator. It is well maintained and its API is simple and compact.
It's quite necessary to validate dates. With the default Rails form helpers you could select dates like September 31st.
Related
I have created a custom validator which has it's own specific unit tests to check that it works.
Using should-matchers there was a suggestion to add a validates_with matcher, so you could write:
subject.validates_with(:custom_validator)
Quite rightly the suggestion was declined, since it does not really test the behaviour of the model.
But my model has 4 fields that use the custom validator, and I want that behaviour to be tested - ie that those 4 fields are being validated, just as I am testing that they are being validated for presence:
describe '#attribute_name' do
it { is_expected.to validate_presence_of(:attribute_name) }
end
So how can I write a test that basically does the same thing, something sort of like this:
describe '#attribute_name' do
it { is_expected.to use_custom_validator_on(:attribute_name) }
end
This question asks the same thing and the answer suggests building a test model. However, my validator requires an option, it is used like this:
\app\models\fund.rb
class Fund < ActiveRecord
validates :ein, digits: { exactly: 9 }
end
So if I build a test model, and test it as suggested:
it 'is has correct number of digits' do
expect(build(:fund, ein: '123456789')).to be_valid
end
it 'is has incorrect number of digits' do
expect(build(:fund, ein: '123').to be_invalid
end
I receive RecordInvalid error (from my own validator! lol) saying I did not supply the required option for the validator. That option is called 'exactly'.
1) Fund#ein validates digits
Failure/Error: raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
ActiveRecord::RecordInvalid:
Record invalid
So is Rspec not 'seeing' the value '9' defined in the model file?
Obviously it makes no sense to define that in the test as that is the defined behaviour I am trying to test for. Think of it like the validates_length_of testing for the { length: x } option.
Surely there must be a way to test that this custom validator option is set on the model?
The validator code
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] << (options[:message] || error_msg(length))
end
private
def error_msg(length)
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
end
Interesting side note
Obviously if I remove the 'raise' line from the DigitsValidator then both the tests succeed. Is there something wrong with my code that I cannot see?
I think you would have to add a return statement, no? :-)
def error_msg(length)
return I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length) if length
raise ActiveRecord::RecordInvalid # option :exactly was not provided (incorrect usage)
end
Alternatively, remove that method and use a guard after setting length:
class DigitsValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
length = options[:exactly]
raise ActiveRecord::RecordInvalid if length.nil?
regex = /\A(?!0{#{length}})\d{#{length}}\z/
return unless value.scan(regex).empty?
record.errors[attribute] <<
(options[:message] ||
I18n.t('activerecord.errors.custom.digits_attr_invalid', length: length))
end
end
I think you should not aim for testing whether the model is using a specific validator. Rather check if the model is valid/invalid in specific cases. In other words, you should be able to test the behaviour of the model without knowing the implementation.
So in this case, you should setup you model correctly with you 'exactly' option for the validator and test if the model validation is sufficient overall.
On the other hand, if you are worried about that someone will misuse the validator in the future and 'exactly' is a required option for the validator, then you should raise error every time when the option is not present and test the validator in isolation like explained here: How to test a custom validator?
I like the idea of not including tests on the model that assume knowledge of exactly what the custom validator is validating. (Otherwise, we'll be repeating logic in the tests for the custom validators, and the tests for the model.)
I solved this by using Mocha (mocking library for Ruby) to set up expectations that the validate_each method of each my custom validators were being called on the correct corresponding field of my model. Simplified example:
Model class:
class User
include ActiveModel::Model
attr_accessor :first_name, :last_name
validates :first_name, first_name: true
validates :last_name, last_name: true
end
Custom validator classes:
class FirstNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
class LastNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# ...
end
end
Model test class:
class UserTest < ActiveSupport::TestCase
def test_custom_validators_called_on_the_appropriate_fields
user = User.new(first_name: 'Valued', last_name: 'Customer')
FirstNameValidator.any_instance.expects(:validate_each).with(user, :first_name, 'Valued')
LastNameValidator.any_instance.expects(:validate_each).with(user, :last_name, 'Customer')
assert_predicate user, :valid?
end
end
I have in my model these two fields - recurring_interval and recurring_time - where I need to validate, that these two fields will be set up both or none of them.
If user will want to set up recurring, he will need to set up both these columns. But if he doesn't, he will leave these columns empty.
How to do that?
class User < ActiveRecord::Base
validates_with Recurring_validator
end
class RecurringValidator < ActiveModel::Validator
def validate(record)
if record.recurring_interval.blank? != record.recurring_time.blank?
record.errors[:base] << "Error message"
end
end
end
The advantage of this approach is, you can use the validator class for other models too.
You can add a custom validation:
class MyModel
...
validate :verify_recurring_stuff
..
private
def verify_recurring_stuff
ri = self.recurring_interval
rt = self.recurring_time
if ri.blank? != rt.blank?
self.errors.add(:base, 'Your error message')
end
end
end
I was trying to create a custom validator class in Ruby on Rails that can be expanded. However, I cannot get it to use validation from both the sub-class and super-class.
This example will clarify what I am trying to achieve:
Super-class
class NameValidator < ActiveModel::EachValidator
def validate_each (record, attribute, value)
#Checks to see if the incoming string is free of any numerical characters
if value.match(/\A[+-]?\d+\Z/)
record.errors[attribute] << "String must contain no numerical characters"
end
end
end
sub-class
class SevenNameValidator < NameValidator
def validate_each (record, attribute, value)
# Checks and only allows 7 character strings to be validated
if value.length != 7
record.errors[attribute] << "String must be 7 characters exactly"
end
end
end
Model class
class User < ActiveRecord::Base
attr_accessible :firstname
validates :firstname, :seven_name => true
end
so if the string "hello" is tested the resulting error => "Strings must be 7 characters exactly"
However if the string "hello77" is tested, it is validated successfully.
Shouldn't it check from NameValidator first and see that it has digits? If not, how can I get inheritance to work in custom validators? Do I need to use methods within my validator classes? An example would be appreciated, I have searched a lot, but I cannot find an example for custom validators.
Call super in sub-class:
class SevenNameValidator < NameValidator
def validate_each (record, attribute, value)
# Checks and only allows 7 character strings to be validated
if value.length != 7
record.errors[attribute] << "String must be 7 characters exactly"
else
#call super to trigger super class method
super
end
end
end
I think it might an issue with your regular expression. If you're trying to match any string with digits you should have something like /\A\D*\d+\D*\z/ right now you're matching a ton of stuff that I don't think you want.
In my form I have a virtual attributes that allows me to accept mixed numbers (e.g. 38 1/2) and convert them to decimals. I also have some validations (I'm not sure I'm handling this right) that throws an error if something explodes.
class Client < ActiveRecord::Base
attr_accessible :mixed_chest
attr_writer :mixed_chest
before_save :save_mixed_chest
validate :check_mixed_chest
def mixed_chest
#mixed_chest || chest
end
def save_mixed_chest
if #mixed_chest.present?
self.chest = mixed_to_decimal(#mixed_chest)
else
self.chest = ""
end
end
def check_mixed_chest
if #mixed_chest.present? && mixed_to_decimal(#mixed_chest).nil?
errors.add :mixed_chest, "Invalid format. Try 38.5 or 38 1/2"
end
rescue ArgumentError
errors.add :mixed_chest, "Invalid format. Try 38.5 or 38 1/2"
end
private
def mixed_to_decimal(value)
value.split.map{|r| Rational(r)}.inject(:+).to_d
end
end
However, I'd like to add another column, wingspan, which would have the virtual attribute :mixed_wingspan, but I'm not sure how to abstract this to reuse it—I will be using the same conversion/validation for several dozen inputs.
Ideally I'd like to use something like accept_mixed :chest, :wingspan ... and it would take care of the custom getters, setters, validations, etc.
EDIT:
I'm attempting to recreate the functionality with metaprogramming, but I'm struggling in a few places:
def self.mixed_number(*attributes)
attributes.each do |attribute|
define_method("mixed_#{attribute}") do
"#mixed_#{attribute}" || attribute
end
end
end
mixed_number :chest
This sets chest to "#mixed_chest"! I'm trying to get the instance variable #mixed_chest like I have above.
You're going to want a custom validator
Something like
class MixedNumberValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.present? && MixedNumber.new(value).to_d.nil?
record.errors[attribute] << (options[:message] || "Invalid format. Try 38.5 or 38 1/2")
end
end
end
Then you can do
validates :chest, mixed_number: true
Note that I'd extract the mixed_to_decimal stuff into a separate class
class MixedNumber
def initialize(value)
#value = value
end
def to_s
#value
end
def to_d
return nil if #value.blank?
#value.split.map{|r| Rational(r)}.inject(:+).to_d
rescue ArgumentError
nil
end
end
and that this definition lets you drop the if statement in the save_chest method.
Now you just need to do some metaprogramming to get everything going, as I suggested in my answer to your other question. You'll basically want something like
def self.mixed_number(*attributes)
attributes.each do |attribute|
define_method("mixed_#{attribute}") do
instance_variable_get("#mixed_#{attribute}") || send(attribute)
end
attr_writer "mixed_#{attribute}"
define_method("save_mixed_#{attribute}") do
# exercise for the reader ;)
end
before_save "save_#{attribute}"
validates "mixed_#{attribute}", mixed_number: true
end
end
mixed_number :chest, :waist, :etc
Does rails do any validation for datetime? I found a plugin
http://github.com/adzap/validates_timeliness/tree/master,
but it seems like something that should come in out of the box.
There's no built-in ActiveRecord validator for DateTimes, but you can easily add this sort of capability to an ActiveRecord model, without using a plugin, with something like this:
class Thing < ActiveRecord::Base
validate :happened_at_is_valid_datetime
def happened_at_is_valid_datetime
errors.add(:happened_at, 'must be a valid datetime') if ((DateTime.parse(happened_at) rescue ArgumentError) == ArgumentError)
end
end
Gabe's answer didn't work for me, so here's what I did to validate my dates:
class MyModel < ActiveRecord::Base
validate :mydate_is_date?
private
def mydate_is_date?
if !mydate.is_a?(Date)
errors.add(:mydate, 'must be a valid date')
end
end
end
I was just looking to validate that the date is in fact a date, and not a string, character, int, float, etc...
More complex date validation can be found here: https://github.com/codegram/date_validator
Recent versions of Rails will type cast values before validation, so invalid values will be passed as nils to custom validators. I'm doing something like this:
# app/validators/date_time_validator.rb
class DateTimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.public_send("#{attribute}_before_type_cast").present? && value.blank?
record.errors.add(attribute, :invalid)
end
end
end
# app/models/something.rb
class Something < ActiveRecord::Base
validates :sold_at, date_time: true
end
# spec/models/something_spec.rb (using factory_girl and RSpec)
describe Something do
subject { build(:something) }
it 'should validate that :sold_at is datetimey' do
is_expected.not_to allow_value(0, '0', 'lorem').for(:sold_at).with_message(:invalid)
is_expected.to allow_value(Time.current.iso8601).for(:sold_at)
end
end
You can create a custom datetime validator by yourself
1) create a folder called validators in inside app directory
2) create a file datetime_validator.rb. with the following content inside app/validators directory
class DatetimeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if ((DateTime.parse(value) rescue ArgumentError) == ArgumentError)
record.errors[attribute] << (options[:message] || "must be a valid datetime")
end
end
end
3) Apply this validation on model
class YourModel < ActiveRecord::Base
validates :happend_at, datetime: true
end
4) Add the below line in application.rb
config.autoload_paths += %W["#{config.root}/app/validators/"]
5) Restart your rails application
Note: The above method tested in rails 4
I recommend a gem date_validator. See https://rubygems.org/gems/date_validator. It is well maintained and its API is simple and compact.
It's quite necessary to validate dates. With the default Rails form helpers you could select dates like September 31st.