I have written a custom validation function in the rails model. I want to check that validation only a field is changed(in my example the field "activestatus" is changed from "active" to "inactive"). How to do that?
class Normaluser < ApplicationRecord
validate :check_on_update, :on => :update
validate :check_on_status_change :"***what to write here?***"
private
def check_on_update
if is_cyclic?
puts "hierarchy is cyclic"
errors.add(:pid,'it is cyclic')
elsif(get_height(id)+getDepth>=2)
puts "height limit exceeded"
errors.add(:pid,'height limit exceeded')
elsif count_children>=4
errors.add(:pid,'children limit exceeded')
end
puts "all is fine at update"
end
You can use ActiveModel::Dirty
if activestatus_was == 'active' && activestatus == 'inactive'
# validation logic goes here
end
More info: https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
Hope that helps!
Have a look at ActiveModel::Dirty. To only run the validation when activestatus changed from active to inactive just add a line to custom validation method:
def check_on_update
return unless activestatus_changed?(from: 'active', to: 'inactive')
# your validation code
end
Related
In my Purchase model, I have a method that calculates the tax:
def calculate_tax
if self.shipping_address.state == State.new_york
corresponding_tax = Tax.find_by(zip_code: self.shipping_address.zip_code, state_id: self.shipping_address.state_id)
if corresponding_tax
self.tax = corresponding_tax.rate * (self.subtotal + shipping)
else
#HERE !!!
self.errors[:base] << "The zip code you have entered is invalid."
puts "errors = #{self.errors.full_messages}" #<-- this prints out the error in my log, so I know it's being run
end
else
self.tax = 0.00
end
end
This method is being called within this method:
def update_all_fees!
calculate_subtotal
calculate_shipping
calculate_tax #<-- being called here
calculate_total
save!
end
However, save! is saving the record successfully. Shouldn't it be throwing an exception? How would I make it so that save! fails when calculate_tax is in the second else block?
You can add custom validation methods with the validate directive. Here's may take on the code you posted:
class Purchase < ActiveRecord::Base
validate :new_york_needs_tax_record
def update_all_fees!
calculate_subtotal
calculate_shipping
calculate_tax
calculate_total
save!
end
private
def calculate_tax
if ships_to_new_york? && corresponding_tax
self.tax = corresponding_tax.rate * (self.subtotal + shipping)
elsif !ships_to_new_york?
self.tax = 0.00
else
self.tax = nil
end
end
def ships_to_new_york?
self.shipping_address.state == State.new_york
end
def corresponding_tax
Tax.find_by(zip_code: self.shipping_address.zip_code, state_id: self.shipping_address.state_id)
end
def new_york_need_tax_record
if ships_to_new_york? && !corresponding_tax
self.errors[:base] << "The zip code you have entered is invalid."
end
end
end
Edited for historical reasons. The first response didn't cover all scenarios.
But if you need to raise the error if there are any just do:
validate :taxes_scenario
def taxes_scenario
[Add any clause here that makes your scenario invalid]
end
So you can validate the taxes scenario and made sure your error is added properly.
Simply adding an error to the error list won't make the record fail. You have to have what's called a "validation" set up. There are some wonderful guides about it here that should help you through the process.
For example in this one you will probably want to add to your Tax model the following validation:
validates :zip_code, presence: true, numericality: true
once you have the validations set up, save! should automatically spit out an error when a model fails validation
In Rails 7:
class User < ApplicationRecord
validate :must_be_a_friend
def must_be_a_friend
if friend == false
# Does NOT work anymore
errors[:base] << 'Must be friends'
# DOES work
errors.add(:base, 'Must be friends')
end
end
end
I am trying to add a custom error to an instance of my User model, but when I call valid? it is wiping the custom errors and returning true.
[99] pry(main)> u.email = "test#test.com"
"test#test.com"
[100] pry(main)> u.status = 1
1
[101] pry(main)> u.valid?
true
[102] pry(main)> u.errors.add(:status, "must be YES or NO")
[
[0] "must be YES or NO"
]
[103] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={:status=>["must be YES or NO"]}>
[104] pry(main)> u.valid?
true
[105] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={}>
If I use the validate method from within the model, then it works, but this specific validation is being added from within a different method (which requires params to be passed):
User
def do_something_with(arg1, arg2)
errors.add(:field, "etc") if arg1 != arg2
end
Because of the above, user.valid? is returning true even when that error is added to the instance.
In ActiveModel, valid? is defined as following:
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
errors.clear
run_validations!
ensure
self.validation_context = current_context
end
So existing errors are cleared is expected. You have to put all your custom validations into some validate callbacks. Like this:
validate :check_status
def check_status
errors.add(:status, "must be YES or NO") unless ['YES', 'NO'].include?(status)
end
If you want to force your model to show the errors you could do something as dirty as this:
your_object = YourModel.new
your_object.add(:your_field, "your message")
your_object.define_singleton_method(:valid?) { false }
# later on...
your_object.valid?
# => false
your_object.errors
# => {:your_field =>["your message"]}
The define_singleton_method method can override the .valid? behaviour.
This is not a replacement for using the provided validations/framework. However, in some exceptional scenarios, you want to gracefully return an errd model. I would only use this when other alternatives aren't possible. One of the few scenarios I have had to use this approach is inside of a service object creating a model where some portion of the create fails (like resolving a dependent entity). It doesn't make sense for our domain model to be responsible for this type of validation, so we don't store it there (which is why the service object is doing the creation in the first place). However for simplicity of the API design it can be convenient to hang a domain error like 'associated entity foo not found' and return via the normal rails 422/unprocessible entity flow.
class ModelWithErrors
def self.new(*errors)
Module.new do
define_method(:valid?) { false }
define_method(:invalid?) { true }
define_method(:errors) do
errors.each_slice(2).with_object(ActiveModel::Errors.new(self)) do |(name, message), errs|
errs.add(name, message)
end
end
end
end
end
Use as some_instance.extend(ModelWithErrors.new(:name, "is gibberish", :height, "is nonsense")
create new concerns
app/models/concerns/static_error.rb
module StaticError
extend ActiveSupport::Concern
included do
validate :check_static_errors
end
def add_static_error(*args)
#static_errors = [] if #static_errors.nil?
#static_errors << args
true
end
def clear_static_error
#static_errors = nil
end
private
def check_static_errors
#static_errors&.each do |error|
errors.add(*error)
end
end
end
include the model
class Model < ApplicationRecord
include StaticError
end
model = Model.new
model.add_static_error(:base, "STATIC ERROR")
model.valid? #=> false
model.errors.messages #=> {:base=>["STATIC ERROR"]}
A clean way to achieve your needs is contexts, but if you want a quick fix, do:
#in your model
attr_accessor :with_foo_validation
validate :foo_validation, if: :with_foo_validation
def foo_validation
#code
end
#where you need it
your_object.with_foo_validation = true
your_object.valid?
I have a model Phone with checked_by field; if this field is equal to 1, then we know this phone is unchecked, else(>1) - checked. On admin side I can review a list of Phones and I need to create a filter using meta_search to review:
All Phones
Checked
Unchecked
I can see checked_by_greater_than, or checked_by_less_than methods in meta_search, but how to combine those methods in a single select box?
Thanks in any advise
With a scope and a made-up field.
The scope:
class Phone < ActiveRecord::Base
scope :checked, lambda { |value|
!value.zero? ? checked_by_greater_than(1) : where(:checked_by => 1)
}
end
Then add a select-box with three values, returning [nil, 0, 1] as values, and in your controller use that parameter to apply the new scope.
class PhonesController < ApplicationController
def index
# ...
#phones ||= Phone.scoped
checked_select_value = params.delete("checked_select") # here use the name of your form field
if checked_select_value.present?
#phones = #phones.checked(checked_select_value.to_i)
end
# now apply the rest of your meta-search things to the #phones
#
end
end
I have a class with some validations:
class HeyThere < ActiveRecord::Base
validate :check_something
validate :check_something_property_1
validate :check_something_property_2
validate :check_something_property_3
def check_something
errors.add(:something, "not there") if something.nil?
end
def check_something_property_1
errors.add(:something, "bad property 1") if something.property_1 > 10
end
def check_something_property_2
errors.add(:something, "bad property 2") if something.property_2 == "ha!"
end
def check_something_property_3
errors.add(:something, "bad property 3") if something.property_3
end
end
The problem is, if something doesn't exist, the first validation triggers, but the second one throws an exception:
undefined method `property_1' for nil:NilClass
For the example, I gave general examples for the validations, but in reality, they are fairly complex. I could change each one to if something && something.property_N whatever, but that feels hacky, and makes the code less-readable, plus, its not very DRY when the number of validations becomes larger.
Is there a way to cancel the remaining validations if the first one fails?
Since they depend on each other, these shouldn't be separate validations. Do this:
class PastaRecipe < ActiveRecord::Base
validate :have_ingredients
private
def have_ingredients
# Don't run the remaining validations if any one validation fails.
have_pasta &&
have_sauce &&
have_water
end
def have_pasta
errors.add(:pasta, "need to buy pasta!") unless pasta.purchased?
end
def have_sauce
errors.add(:sauce, "need delicious sauce!") unless sauce.delicious?
end
def have_water
errors.add(:water, "need to boil water!") unless water.boiled?
end
end
Since you don't want to use if-else statements, I suggest using raise to halt the entire validation chain:
class HeyThere < ActiveRecord::Base
validate :check_something
validate :check_something_property_1
validate :check_something_property_2
validate :check_something_property_3
def check_something
if something.nil?
errors.add(:something, "not there")
raise ActiveRecord::RecordInvalid, self
end
end
def check_something_property_1
errors.add(:something, "bad property 1") if something.property_1 > 10
end
def check_something_property_2
errors.add(:something, "bad property 2") if something.property_2 == "ha!"
end
def check_something_property_3
errors.add(:something, "bad property 3") if something.property_3
end
end
I did something like this. I'm just using the validations module here, but it applies to active record models the same way. There are a lot of different modifications that can be done to this, but this is almost the exact implementation I used.
Must list validations in order
Can be modified to halt on any error including instance.errors[:base]
Can halt on ANY error using instance.errors.any?
with_options method will actually pass the "unless: Proc" to each validation, so it actually
runs for each validation
with_options can be replaced by just putting if or unless conditions on each validation
Class definition. Non-AR version. Same can be done with ActiveRecord::Base classes
class Skippy
# wouldnt need to do this initialize on the ActiveRecord::Base model version
include ActiveModel::Validations
validate :first
validate :second
validate :halt_on_third
validates_presence_of :or_halt_on_fourth
with_options unless: Proc.new{ |instance| [:halt_on_thirds_error_key, :or_halt_on_fourth].any?{ |key| instance.errors[key].any? } } do |instance|
instance.validate :wont_run_fifth
instance.validates_presence_of :and_wont_run_sixth
end
# wouldn't need to do these on the ActiveRecord::Base model version
attr_accessor :attributes
def initialize
#attributes = { or_halt_on_fourth: "I'm here" }
end
def read_attribute_for_validation(key)
#attributes[key]
end
def first
errors.add :base, 'Base error from first'
end
def second
errors.add :second, 'Just an error'
end
def halt_on_third
errors.add :halt_on_thirds_error_key, 'Halting error' unless #donthalt
end
def wont_run_fifth
errors.add :wont_run_fifth, 'ran because there were no halting errors'
end
end
Demo
2.0.0 :040 > skippy = Skippy.new
=> #<Skippy:0x0000000d98c1c0 #attributes={:or_halt_on_fourth=>"I'm here"}>
2.0.0 :041 > skippy.errors.any?
=> false
2.0.0 :042 > skippy.valid?
=> false
2.0.0 :043 > skippy.errors.full_messages
=> ["Base error from first", "Second Just an error", "Halt on thirds error key Halting error"]
2.0.0 :044 > skippy.errors.clear
=> {}
2.0.0 :045 > skippy.instance_variable_set(:#donthalt, true)
=> true
2.0.0 :046 > skippy.errors.any?
=> false
2.0.0 :047 > skippy.valid?
=> false
2.0.0 :048 > skippy.errors.full_messages
=> ["Base error from first", "Second Just an error", "Wont run fifth ran because there were no halting errors", "And wont run sixth can't be blank"]
2.0.0 :049 > skippy.errors.clear; skippy.attributes = {}; skippy.errors.any?
=> false
2.0.0 :050 > skippy.valid?; skippy.errors.full_messages
=> ["Base error from first", "Second Just an error", "Or halt on fourth can't be blank"]
2.0.0 :051 >
In my form I would like users to type a date in DD/MM/YYYY format. I have validation that checks that.
The problem is that in the database it is stored in YYYY-MM-DD format, so if try just to update the is_money_paid field:
job = Job.find(params[:id])
job.update_attributes(:is_money_paid => ...)
the validation fails saying that job's date is in wrong format (YYYY-MM-DD rather than DD/MM/YYYY).
What would be an appropriate way to solve this ?
Here is the relevant code:
class Job < ActiveRecord::Base
validate :job_date_validator
end
DATE_REGEX = /\A\d{2}\/\d{2}\/\d{4}\z/
def job_date_validator
if job_date_before_type_cast.blank?
errors.add(:job_date, "^Date must be selected")
elsif job_date_before_type_cast !~ DATE_REGEX
errors.add(:job_date, "^Date must be in DD/MM/YYYY format")
end
end
is_money_paid is a boolean field.
I would change the validator to say:
validate_presence_of :job_date, :message => "must be selected"
validate :job_date_validator, :if => :job_date_changed?
Or something along those lines.
You can take out the .blank? check too.
Format it in before_validation
http://guides.rubyonrails.org/active_record_validations_callbacks.html#updating-an-object
I would suggest you to see gem validates_timeliness which is rails 3 compatible.