I have a custom validator that I want to apply to several attributes in the same model
right now I the following the works just fine:
validates :first_name, validator_name: true
validates :age, validator_name: true
validates :gender, validator_name: true
But when I try:
validates :first_name, :age, :gender, validator_name: true
The validator will run for the first attribute (:first_name) but not the others. Is this possible to achieve? I spent hours googling this but haven't found any examples
module Person
class SomeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless can_do_something?(record, attribute)
#... more code
end
def can_do_something?(record, attribute)
anything_new = record.new_record? || record.attribute_changed?(attribute)
end
end
end
Not sure if this should just be a comment or if it constitutes an answer; however what you are requesting...
I have a custom validator that I want to apply to several attributes in the same model
...is how an EachValidator works.
So what you are describing...
The validator will run for the first attribute (:first_name) but not the others.
..cannot be accurate.
For Example:
require 'active_model'
class StartsWithXValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.match?(/^(?:\d+\s|^)X/)
record.errors.add attribute, "must start with X"
end
end
end
class Person
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :name, :city, :street
validates :name, :city, :street, starts_with_x: true
end
In this case all three attributes will be validated through the StartsWithXValidator.
e.g.
person = Person.new({name: 'Xavier', city: 'Xenia', street: '123 Xenial St'})
person.valid?
#=> true
person_2 = Person.new({name: 'Benjamin', city: 'Philadelphia', street: '700 Market St'})
person_2.valid?
#=> false
person_2.errors.full_messages
#=> ["Name must start with X", "City must start with X", "Street must start with X"]
Working Example
I think you can use a custom method validation:
validate :validate_person
def validate_person
[:first_name, :age, :gender].each do |attr|
validates attr, validator_name: true
end
end
Reference: https://guides.rubyonrails.org/active_record_validations.html#custom-methods
Related
assuming i have two models human and male, they both have similar attributes but not all the attributes, for example:
class Human < ApplicationRecord
has_many :males
validates :name, presence: true
validates :age, presence: true
validates :date_of_birth, presence: true
validates :identity, presence: true
end
class Male < ApplicationRecord
belongs_to :human
validates :name, presence: true
validates :age, presence: true
end
now what i want to do is when i use human.males.build i want the new male instance to inherit the shared attributes like name and age
instead of using this behavior human.males.build({attributes})
I'm thinking of overwriting the initialize (alias of build) method of Male model which will do the trick:
def initialize(attributes = {}, &block)
super(attributes, &block)
self.name = human.name unless attributes[:name].present?
self.age = human.age unless attributes[:age].present?
end
But honestly, I don't think this is a safe and generic way for the problem.
Sorry I'm new to rails but can't wrap my head around this one.
I have an Order object with various attributes - no references
In my controller I can print out the attributes individually via their attr_accessor and see them in the console via puts.
But when I call .inspect they are all nil! any suggestions?
class Order < ApplicationRecord
attr_accessor :name, :email, :phone, :date, :dessert_type, :size, :quantity, :dessert, :comments, :total
validates :name, :date, :quantity, presence: true
validates :quantity, numericality: { only_integer: true, greater_than: 0}
validate :contact_provided?
private
def contact_provided?
if :email.blank? || :phone.blank?
errors.add(:base, "Please provide either phone or email so we can contact you!")
end
end
end
Controller
def create_order
puts "create_order object"
#order = Order.new order_params
if #order.valid?
puts #order.inspect
#everything is null here
#order.attributes.each do |attr_name, attr_value|
puts "#{attr_name}: #{attr_value}"
end
#this prints out fine!
puts "dessert: #{#order.dessert}"
end
end
Parameters
Parameters: {"utf8"=>"✓", "authenticity_token"=>"randomtoken", "order"=>{"name"=>"jim", "email"=>"test#email.com", "phone"=>"12345678", "dessert_type"=>"Cake", "size"=>"25.0", "dessert"=>"Chocolate Caramel", "date"=>"2018-04-15", "quantity"=>"1", "comments"=>""}, "commit"=>"Submit Order"}
Any insight much appreciated!
That's because this line:
attr_accessor :name, :email, :phone, :date, :dessert_type, :size, :quantity, :dessert, :comments, :total
is overriding the Order attributes in the way Rails works with them. As working with Rails you don't need that declaration, so you can remove them as attr_accessor.
As Order is an ActiveRecord model, then the getters and setters are already generated by ActiveRecord for all of your object attributes.
What you're doing right now is defining all of your attributes, with the attr_accessor as virtual attributes which are attributes on the model that don't persist in the database, just "an attribute not corresponding to a column in the database".
I have field date in the User model. It shuld have only future dates (from registration moment). What is the best way to validation it in the model?
I think about something like this:
class User < ActiveRecord::Base
validates :name, presence: true
validates :date, presence: true
validate :future_event
private
def future_event
errors.add(:date, "Can't be in the past!") if date < Time.now
end
end
Is it OK?
Can you think about some more simple and elegant solution?
# lib/future_validator.rb
class FutureValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record[attribute] < Time.now
record.errors[attribute] << (options[:message] || "can't be in the past!")
end
end
end
class User < ActiveRecord::Base
validates :name, presence: true
validates :date, presence: true
validates :future_event, future: true
end
http://guides.rubyonrails.org/active_record_validations.html#custom-validators
Take a look at the Date Validator Gem
validates :start_date,
date: { after: Proc.new { Date.current }, message: 'must be after today' },
on: :create
You might always want to investigate the differences between Date.current and Date.today - Date.current is timezone aware and will use the Timezone of your Rails app. Date.today uses system time. There can be odd differences if one is UTC and one is Eastern.
Use inclusion.
Something like this:
validates :date, inclusion: { in: Proc.new{ 1.day.from_now.. }, message: "has to be in the future" }
This is my model class:
class Availability < ActiveRecord::Base
attr_accessible :beginning_date, :end_date
validates :beginning_date, :end_date :presence => true
# custom validators
validate :dates_cant_be_in_the_past
def dates_cant_be_in_the_past
if Date.parse(beginning_date) < Date.today
errors.add(:beginning_date, "cant be in the past")
end
if Date.parse(end_date) < Date.today
errors.add(:end_date, "cant be in the past")
end
end
end
Now two things should happen: At first validate the presence of the beginning_date and end_date attributes and than run my dates_cant_be_in_the_pastvalidator.
Sadly this approach doesn't work. If I leave a field empty the Date.parsemethod throws an exception, because the argument is obviously empty.
Is it possible to define the order of default and custom validations? Or do I have to implement the presence validator myself, so I would do something like:
validate :dates_cant_be_blank, :dates_cant_be_in_the_past
The guide at least says:
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.
Thank in advance
It's much simpler if you create a validator for that:
class DateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if Date.parse(value) < Date.today
record.errors.add(attribute, "cant be in the past")
end
end
end
And at your model you would use it like this:
class Availability < ActiveRecord::Base
attr_accessible :beginning_date, :end_date
validates :beginning_date, :end_date :presence => true
validates :beginning_date, :end_date, :date => true, :allow_blank => true
end
The :allow_blank piece if the one prevents the validation from running if the value is empty. Using a real validator object also removes the code form your model making it much simpler and removing the duplication you currently have.
You could try something like this
class Availability < ActiveRecord::Base
attr_accessible :beginning_date, :end_date
validates :beginning_date, :end_date : presence => true
# custom validators
validate :valid_dates
def valid_dates
if valid_string(beginning_date)
errors.add(:beginning_date, "Can't be in the past") unless Date.parse(beginning_date) > Date.today
end
if valid_string(end_date)
errors.add(:end_date, "Can't be in the past") unless Date.parse(end_date) > Date.today
end
end
def valid_string(test_value)
test.value.is_a? String
end
end
After searching for a tableless model example I came across this code which seems to be the general consensus on how to create one.
class Item < ActiveRecord::Base
class_inheritable_accessor :columns
self.columns = []
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
def all
return []
end
column :recommendable_type, :string
#Other columns, validations and relations etc...
end
However I would also like it to function, as a model does, representing a collection of object, so that I can do Item.all.
The plan is to populate Items with files and each Item's properties will be extracted from the files.
However currently if I do Item.all I get a
Mysql2::Error Table 'test_dev.items' doesn't exist...
error.
I found an example at http://railscasts.com/episodes/219-active-model where I can use model features and then override static methods like all (should have thought of this before).
class Item
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
class << self
def all
return []
end
end
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
Or you could do it like this (Edge Rails only):
class Item
include ActiveModel::Model
attr_accessor :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
end
By simply including ActiveModel::Model you get all the other modules included for you. It makes for a cleaner and more explicit representation (as in this is an ActiveModel)