I need a conditional validation in some parts of my app. Right now I am using the following scheme:
User.create
User::WithPassword.create
User::WithPhone.create
It would be cool if I could change class behaviour on the fly like this:
User.with_phone.with_password.create
So I tried to do it like this:
class User < ActiveRecord::Base
validates :phone, presence: true, if: :phone_required?
def self.with_phone
define_method(:phone_required?) { true }
self
end
private
def phone_required?
false
end
end
So it can be used like this where needed:
User.with_phone.create(user_params)
The problem with this approach is that all instances of User get new behaviour since the actual class changes.
Is there a way to return only the modified copy of User class with new instance method phone_required? without affecting the "base" class?
Update
Thank you for the comments as this was more of an idea, the requirement is that I create users without certain validation automatically, and then when they edit profile, they are dealing with pristine User model. I create with_/without_ on the fly in method missing when needed.
Here's my next iteration:
class User < ActiveRecord::Base
validates :phone, presence: true, if: :phone_required?
def self.with_password
define_singleton_method(:password_required?) { true }
self
end
def password_required?
self.class.try :password_required?
end
end
Apparently it's not any better as the singleton method stays there all the time.
Why not simply use an instance variable initialized at creation time?
class User < ActiveRecord::Base
validates :phone, presence: true, if: :phone_required?
#phone_required = false
def self.create_with_phone(params)
obj = self.create(params)
obj.phone_required = true
end
private
def phone_required=(v)
#phone_required = v
end
def phone_required?
#phone_required
end
end
User.create_with_phone(user_params)
Related
What is wrong with my model that I can't see the #black? method?
Error:
undefined method `black?' for Car:Class
Car Modal
# frozen_string_literal: true
class Cars < Base
if black?
validates :style, presence: true
end
private
def black?
color_options.include?("black")
end
end
If I do...
validates :style, presence: true, if: :seller?
The code works, but I plan on adding many more validations in the if statement block, so it would be nice to get the if statement block (if end) working.
You're calling black? on the class, not on an instance of the class. At the point that your if black? code is called, self evaluates to Cars! It wouldn't make any sense to call Cars.black? - you want to know if a single instance of the car is black!
It is good practice to wrap all your various validation conditions into a single predicate which you then reference with the :if syntax.
For example, if you had a complex set of conditions, you might set up a validator:
class Cars
validates :style, presence: true, if: :validate_style?
private
def validate_style?
color_options.include?("black") ||
wheel_options.include?("premium") ||
Time.now.wday == 3
end
end
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)
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!
In Bryan Helmkamp's excellent blog post called "7 Patterns to Refactor Fat ActiveRecord Models", he mentions using Form Objects to abstract away multi-layer forms and stop using accepts_nested_attributes_for.
Edit: see below for a solution.
I've almost exactly duplicated his code sample, as I had the same problem to solve:
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :account
attribute :name, String
attribute :account_name, String
attribute :email, String
validates :email, presence: true
validates :account_name,
uniqueness: { case_sensitive: false },
length: 3..40,
format: { with: /^([a-z0-9\-]+)$/i }
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
One of the things different in my piece of code, is that I need to validate the uniqueness of the account name (and user e-mail). However, ActiveModel::Validations doesn't have a uniqueness validator, as it's supposed to be a non-database backed variant of ActiveRecord.
I figured there are three ways to handle this:
Write my own method to check this (feels redundant)
Include ActiveRecord::Validations::UniquenessValidator (tried this, didn't get it to work)
Or add the constraint in the data storage layer
I would prefer to use the last one. But then I'm kept wondering how I would implement this.
I could do something like (metaprogramming, I would need to modify some other areas):
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
errors.add(:name, "not unique" )
false
end
But now I have two checks running in my class, first I use valid? and then I use a rescue statement for the data storage constraints.
Does anyone know of a good way to handle this issue? Would it be better to perhaps write my own validator for this (but then I'd have two queries to the database, where ideally one would be enough).
Creating a custom validator may be overkill if this just happens to be a one-off requirement.
A simplified approach...
class Signup
(...)
validates :email, presence: true
validates :account_name, length: {within: 3..40}, format: { with: /^([a-z0-9\-]+)$/i }
# Call a private method to verify uniqueness
validate :account_name_is_unique
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
# Refactor as needed
def account_name_is_unique
if Account.where(name: account_name).exists?
errors.add(:account_name, 'Account name is taken')
end
end
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
def setup(klass)
super
#klass = options[:model] if options[:model]
end
def validate_each(record, attribute, value)
# UniquenessValidator can't be used outside of ActiveRecord instances, here
# we return the exact same error, unless the 'model' option is given.
#
if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
# If we're inside an ActiveRecord class, and `model` isn't set, use the
# default behaviour of the validator.
#
elsif ! options[:model]
super
# Custom validator options. The validator can be called in any class, as
# long as it includes `ActiveModel::Validations`. You can tell the validator
# which ActiveRecord based class to check against, using the `model`
# option. Also, if you are using a different attribute name, you can set the
# correct one for the ActiveRecord class using the `attribute` option.
#
else
record_org, attribute_org = record, attribute
attribute = options[:attribute].to_sym if options[:attribute]
record = options[:model].new(attribute => value)
super
if record.errors.any?
record_org.errors.add(attribute_org, :taken,
options.except(:case_sensitive, :scope).merge(value: value))
end
end
end
end
You can use it in your ActiveModel classes like so:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model class has validations as well. Those validations aren't run when you call Signup.new.save, so you will have to check those some other way. You can always use save(validate: false) inside the above persist! method, but then you have to make sure all validations are in the Signup class, and keep that class up to date, when you change any validations in Account or User.
Assuming that I have a class such as the following:
class Book < ActiveRecord::Base
validates :title, :length => {:maximum => 10}
end
Is there a way (gem to install?) that I can have ActiveRecord automatically truncate values according to maximum length?
For instance, when I write:
b = Book.new
b.title = "123456789012345" # this is longer than maximum length of title 10
b.save
should save and return true?
If there is not such a way, how would you suggest that I proceed facing such a problem more generally?
Well, if you want the value truncated if its too long, you don't really need a validation, because it will always pass. I'd handle that like this:
class Book < ActiveRecord::Base
before_save :truncate_values
def truncate_values
self.title = self.title[0..9] if self.title.length > 10
end
end
I have come up with a new validator that does truncation. Here is how I did that:
I created the "validators" folder inside "app" folder and then created the file "length_truncate_validator.rb" with the following content:
class LengthTruncateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ml = options[:maximum]
record.send("#{attribute}=", value.mb_chars.slice(0,ml)) if value.mb_chars.length > ml unless value.nil? or ml.nil?
end
class << self
def maximum(record_class, attribute)
ltv = record_class.validators_on(attribute).detect { |v| v.is_a?(LengthTruncateValidator) }
ltv.options[:maximum] unless ltv.nil?
end
end
end
And inside my model class I have something like:
class Book < ActiveRecord::Base
validates :title, :length_truncate => {:maximum => 10}
end
which is quite handy and works the way I require.
But still, if you think that this one can be improved or done in another way, you are welcome.
This may not have been an option in 2011, but now there's a before_validation callback that will work.
class Book < ApplicationRecord
before_validation do
if self.params && self.params.length > 1000
self.params = self.title[0...10]
end
end
validate :title, length: { maximum: 10 }, allow_nil: true
end
I like the idea of using the before_validation callback. Here's my stab that automatically truncates all strings to within the database's limit
before_validation :truncate_strings
def truncate_strings
self.class.columns.each do |column|
next if column.type != :string
if self[column.name].length > column.limit
self[column.name] = self[column.name][0...column.limit]
end
end
end