Conditional validations in Rails - ruby-on-rails

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

Related

Use string as date input with validation [duplicate]

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.

Rails model validation which check if already exists object with some property

I have following model:
class TestModel < ActiveRecord::Base
enum some_column: [:prop1, :prop2, :prop3]
end
I want to ensure i will always have maximum one object with prop1 column value in database, while i can have multiple numbers of object with prop2 or prop3 objects or none of them. It would be great if it could be a model validation. I tried this way but not sure if this is good practice for a rails app:
if ((id.nil? and TestModel.where(some_column: 'prop1')) or (TestModel.where(some_column: 'prop1').where.not(id: id)))
I tried with left side to cover create action, with right side to cover update. Is there any rails way to do this?
Thanks
UPDATED
This is solution for my problem
def only_one_prop1_record
if(some_column == 'prop1')
if new_record? #if create action
if TestModel.where(some_column: 'prop1').any?
errors.add(:base, 'You can only have one prop1 record')
end
elsif persisted? #if update action
if TestModel.where(some_column: 'prop1').where.not(id: id).any?
errors.add(:base, 'You can only have one prop1 record')
end
end
end
end
And call validation like this: validate :only_one_prop1_record
This might help you:
class TestModel < ActiveRecord::Base
enum some_column: [:prop1, :prop2, :prop3]
validate :only_one_prop1_record, on: :create
def only_one_prop1_record
if TestModel.exists?(some_column: prop1)
errors.add(:base, 'You can only have one prop1 record')
end
end
end
Or may be to run the validation on update and create both you can try this:
def only_one_prop1_record
existing_record = TestModel.where(some_column: prop1).first
if (new_record? && existing_record.present?) || (persisted? && existing_record != self)
errors.add(:base, 'You can only have one prop1 record')
end
end
And remove the on: :create from above.

DRY up this model with virtual attributes

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

looking for gem that flags datetime fields

I got datetime filds order_confirmed_at and completion_confirmed_at
class CoolModel < ActiveRecord::Base
attr_accessible :order_confirmed, completion_confirmed
def order_confirmed
order_confirmed_at.present?
end
def order_confirmed=(state)
if state and order_confirmed_at.blank?
self.order_confirmed_at = Time.now
end
order_confirmed_at.present?
end
def completion_confirmed
completion_confirmed_at.present?
end
def completion_confirmed=(state)
if state and completion_confirmed_at.blank?
self.completion_confirmed_at = Time.now
end
completion_confirmed_at.present?
end
end
...so in my view I can just check checkbox that order was confirmed and completed
Thing is: not only this is duplication, but this stuff obviously looks pretty standard. So in matter saving me time writing gem: is there rails gem/engine doing this (or maybe part of Rails I'm not aware of) ??
class CoolModel < ActiveRecord::Base
#something like this
acts_even_coller_on :order_confirmed_at, :completion_confirmed_at
end
You could just override the setter method.
def confirmed_at=(value)
super value == "true" ? confirmed_at || Time.now : nil
end

rails built in datetime validation

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.

Resources