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
Related
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.
I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
I try to optimize the following Accounting model class and have two questions:
1) How can i replace the multiple attribute setters with a more elegant method?
2) Is there a better way than the if conditional in the replace_comma_with_dot method
class Accounting < ActiveRecord::Base
validates :share_ksk, :central_office, :limit_value, :fix_disagio,
presence: true, numericality: { less_than: 999.99, greater_than_or_equal_to: 0.00 },
format: { with: /\d*\.\d{0,2}$/, multiline: true, message: I18n.t('accounting.two_digits_after_decimal_point')}
def share_ksk=(number)
replace_comma_with_dot(number)
super
end
def central_office=(number)
replace_comma_with_dot(number)
super
end
def limit_value=(number)
replace_comma_with_dot(number)
super
end
def fix_disagio=(number)
replace_comma_with_dot(number)
super
end
def replace_comma_with_dot(number)
if number.is_a? String
number.sub!(",", ".")
elsif number.is_a? Float
number
else
""
end
end
end
As user Pardeep suggested i'm trying to replace my getters with define_method:
[:share_ksk=, :central_office=, :limit_value=, :fix_disagio=].each do |method_name|
self.class.send :define_method, method_name do |number|
replace_comma_with_dot(number)
super
end
end
What am i missing?
You can define methods dynamically using the define_method, you can fine more information here
and you can update your replace_comma_with_dot with this
def replace_comma_with_dot(number)
return number.sub!(",", ".") if number.is_a? String
return number if number.is_a? Float
""
end
end
Instead of having a method in your model, I'd extract the functionality and append it to either the String or Integer classes:
#lib/ext/string.rb
class String
def replace_comma_with_dot
number.sub!(",",".") #Ruby automatically returns so no need to use return
end
end
This - if your number is a string will allow you to do the following:
number = "67,90"
number.replace_comma_with_dot
To use this in the app, the setters are okay. You could achieve your functionality as follows:
def fix_disagio=(number)
self[:fix_disagio] = number.replace_comma_with_dot
end
Your update is okay, except I would shy away from it myself as it creates bloat which doesn't need to be there.
I was looking for a way to set the attributes when you pull from the db, but then I realized that if you're having to set this each time you call the model, surely something will be wrong.
I would personally look at changing this at db level, failing that, you'd probably be able to use some sort of localization to determine whether you need a dot or comma.
There is a good answer here which advocates adding to the ActiveRecord::Base class:
class ActiveRecord::Base
def self.attr_localized(*fields)
fields.each do |field|
define_method("#{field}=") do |value|
self[field] = value.is_a?(String) ? value.to_delocalized_decimal : value
end
end
end
end
class Accounting < ActiveRecord::Base
attr_localized :share_ksk
end
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
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.