Why can't I reuse a custom validator in Rails? - ruby-on-rails

I have a custom validator in lib/validators/past_validator.rb that looks like that:
class PastValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
(record.errors[attribute] << "must be in the past") if value >= Date.today
end
end
I required this in application.rb like so:
config.autoload_paths += %W(#{config_root}/lib/validators)
I want to use this validator in two models user and photo. A user has a birthday(date) and a photo has a date-attribute 'taken_on', both must be in the past.
In user.rb I have the following line:
validates :birthday, allow_nil: true, past: true
The validation of birthday works like charm here.
In photo.rb:
def self.new_photo(params)
photo = new(taken_on: (Date.parse params[:taken_on]) )
photo
end
validates :taken_on, presence: true, past: true
If i comment out "past: true", this also works, but if not i get NoMethodError. The backtrace showed me, that value is not set in the validator, so the interpreter thinks value must be a method and gives me a NoMethodError. But why is value not set?
I already tried to do the following in photo.rb:
def self.new_photo(params)
date = Date.parse params[:taken_on]
p date # Gives me a correct instance of Date
photo = new(taken_on: date)
photo
end
validates :taken_on, presence: true, past: true, on: :create
The strange thing is that validation happens already when i instanciate a photo.

Related

How to use validations with mobility gem?

I'm trying to add validations to my mobility-powered application and i'm confused a little.Earlier I've used code like this
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, uniqueness: {scope: :animal_type}
end
And it worked fine. But in my last project it doesn't work at all. Any ideas how to perform validations? My config is below:
Mobility.configure do
plugins do
backend :container
active_record
reader
writer
backend_reader
query
cache
presence
locale_accessors
end
end
UPD: I've identified my problem - it is because of , uniqueness: {scope: :animal_type}. Is it possible to use mobility with similar type of validations?
When you use uniqueness validator it uses query to the database to ensure that record haven't already taken and you get this query:
Let's assume you have Animal model
SELECT 1 AS one FROM "animals" WHERE "animals"."name_en" = "Cat" AND "animals"."animal_type" = "some_type" LIMIT 1
And of course there is no name_en field in the animals table that is why you have an error
To archive this you have to write your own validator
class CustomValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if translation_exists?(record, attribute)
record.errors.add(attribute, options[:message] || :taken)
end
end
private
def translation_exists?(record, attribute)
attribute, locale = attribute.to_s.split('_')
record.class.joins(:string_translations).exists?(
mobility_string_translations: { locale: locale, key: attribute },
animals: { animal_type: record.animal_type }
)
end
end
And then in your model do next:
I18n.available_locales.each do |locale|
validates :"name_#{locale}", presence: true, custom: true
end

How to set the order of validation methods ruby on rails?

I am a Ror beginner. There is a model Lecture, I want to validate the format of start time and end time firstly, and then check if the end time is after start time. It works well when the format is valid but once the format is wrong it comes with: undefined method `<' for nil:NilClass. How to makes start_must_be_before_end_time triggered only when the format is valid? Thanks!
Here is the code:
class Lecture < ActiveRecord::Base
belongs_to :day
belongs_to :speaker
validates :title, :position, presence: true
validates :start_time, format: { with: /([01][0-9]|2[0-3]):([0-5][0-9])/,
message: "Incorrect time format" }
validates :end_time, format: { with: /([01][0-9]|2[0-3]):([0-5][0-9])/,
message: "Incorrect time format" }
validate :start_must_be_before_end_time
private
def start_must_be_before_end_time
errors.add(:end_time, "is before Start time") unless start_time < end_time
end
end
There are no guarantees on the order of validations that are defined by validates methods. But, the order is guaranteed for custom validators that are defined with validate method.
From docs:
You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered.
Alternatively
You can only run your validation method if all other validations pass:
validate :start_must_be_before_end_time, :unless => Proc.new { |obj| obj.times_valid? }
# Then, define `times_valid?` method and check that start_time and end_time are valid
You can go another way:
errors.add(:end_time, "is before Start time") unless start_time.nil? || end_time.nil? || start_time < end_time

Changing data format with before_validation doesn't work

I'm saving some attributes of my model instances as floats, but it's necessary for my app to handle an input from a form with commas instead of points (ex. 10,99 should be saved as 10.99).
I'm trying to do that with before_validation callbacks, but I can't make it work properly - input with commas can't get through validations, I keep getting price/size2 is not a number error.
class Advert < ActiveRecord::Base
before_validation do
normalize(self.price) if is_number?(self.price)
normalize(self.size2) if is_number?(self.size2)
end
validates :price, presence: true, numericality: true
validates :size2, numericality: true
def is_number?(string)
true if Float(string) rescue false
end
def normalize(number)
number.to_s.gsub!(',', '.').to_f
end
Any help would be appreciated.
I havent tested it but i think you are not actually modifying price and size2 as you are gsub!ing number and not returning the value to the 2 attributes.
what about something like this:
class Advert < ActiveRecord::Base
before_validation :string_to_float
validates :price, presence: true, numericality: true
validates :size2, numericality: true
def is_number?(string)
true if Float(string) rescue false
end
def normalize(number)
number.to_s.gsub(',', '.').to_f
end
def string_to_float
self.price = normalize(self.price) if is_number?(self.price)
self.size2 = normalize(self.size2) if is_number?(self.size2)
end
end
Use a custom setter method instead:
class Advert
def price=(val)
#price = val.is_a?(Float) ? val : val.to_f
end
end
As I couldn't handle the decimal delimiter change via rails callbacks, I just wrote a simple jQuery workaround, maybe it'll help someone:
normalize_input = ->
$('form').submit ->
normalized_price = $('#advert_price').val().replace(',', '.') # advert_price is a form input
normalized_size2 = $('#size2input').val().replace(',', '.') # just like the size2input
$('#advert_price').val(normalized_price)
$('#size2input').val(normalized_size2)

Rails: how to validate an object field's value before save?

I'm writing a Redmine plugin that should check if some fields of an Issue are filled depending on values in other fields.
I've written a plugin that implements validate callback, but I don't know how to check field values which are going to be saved.
This is what I have so far:
module IssuePatch
def self.included(receiver)
receiver.class_eval do
unloadable
validate :require_comment_when_risk
protected
def require_comment_when_risk
risk_reduction = self.custom_value_for(3)
if risk_reduction.nil? || risk_reduction.value == 0
return true
end
comment2 = self.custom_value_for(4)
if comment2.nil? || comment2.value.empty?
errors.add(:comment2, "Comment2 is empty")
end
end
end
end
end
The problem here is that self.custom_value_for() returns the value already written to the DB, but not the one that is going to be written, so validation doesn't work. How do I check for the value that was passed from the web-form?
Any help will be greatly appreciated.
The nice thing about rails is that in your controller you don't have to validate anything. You are suppose to do all of this in your model. so in your model you should be doing something like
validates :value_that_you_care_about, :numericality => { :greater_than_or_equal_to => 0 }
or
validates :buyer_name, presence: true, :length => {:minimum => 4}
or
validates :delivery_location, presence: true
If any of these fail this will stop the object from being saved and if you are using rails scaffolding will actually highlight the field that is incorrect and give them and error message explaining what is wrong. You can also write your own validations such as
def enough_red_flowers inventory
if inventory.total_red_flowers-self.red_flower_quantity < 0
self.errors.add(:base, 'There are not enough Red Flowers Currently')
return false
end
inventory.total_red_flowers = inventory.total_red_flowers-self.red_flower_quantity
inventory.save
true
end
To write your own custom message just follow the example of self.errors.add(:base, 'your message')
You can find more validations here
Better way it's create custom validator
class FileValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# some logic for validation
end
end
then in model:
validates :file, file: true

ActiveRecord validation statement containing ||

I've done some searching online, and understand how ActiveRecord validations can be built with "if" statements and separately defined methods. However, I'm wondering if it's possibly to simply combine two validations together, and if either is true, the whole thing passes.
What I'm trying to do is have a user input a contact field that can either be an email or a phone number, but not both. Obviously the code I have below isn't working, but I'm wondering if something similar to it could work?
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
VALID_PHONE = /\d{10}/
validates :contact, presence: true, format: { with: VALID_EMAIL_REGEX } || presence: true, length: { is: 10 }, format: { with: VALID_PHONE }
I was going to recommend using a standard :if option for the validates callback, but having done a little more research, I found something you may benefit from:
Custom Validations
According to the Rails guide:
#app/models/concerns/my_validator.rb
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
#app/models/person.rb
class Person
include ActiveModel::Validations
validates_with MyValidator
end
This allows you to create your own validation method, allowing you to append error messages directly into the instance variable (which is then shown on the form). For your question, I'd to do this:
#app/models/concerns/phone_email_validator.rb
class PhoneEmailValidator < ActiveModel::Validator
def validate(record)
contact = record.contact
phone = /\d{10}/
email = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
if contact.validate(phone) || contact.validate(email)
if contact.validate(phone) && contact.length < 10
error = "Length too short for phone number!"
end
else
error = 'Needs To Be Phone Or Email '
end
record.errors[:contact] << error
end
end
#app/models/person.rb
class Person
include ActiveModel::Validations
validates_with MyValidator
end
The function is too verbose, and might not work with the validation regex; but it's an idea either way!

Resources