Hi I have an array column in my model:
t.text :sphare, array: true, default: []
And I want to validate that it includes only the elements from the list ("Good", "Bad", "Neutral")
My first try was:
validates_inclusion_of :sphare, in: [ ["Good"], ["Bad"], ["Neutral"] ]
But when I wanted to create objects with more then one value in sphare ex(["Good", "Bad"] the validator cut it to just ["Good"].
My question is:
How to write a validation that will check only the values of the passed array, without comparing it to fix examples?
Edit added part of my FactoryGirl and test that failds:
Part of my FactoryGirl:
sphare ["Good", "Bad"]
and my rspec test:
it "is not valid with wrong sphare" do
expect(build(:skill, sphare: ["Alibaba"])).to_not be_valid
end
it "is valid with proper sphare" do
proper_sphare = ["Good", "Bad", "Neutral"]
expect(build(:skill, sphare: [proper_sphare.sample])).to be_valid
end
Do it this way:
validates :sphare, inclusion: { in: ["Good", "Bad", "Neutral"] }
or, you can be fancy by using the short form of creating the array of strings: %w(Good Bad Neutral):
validates :sphare, inclusion: { in: %w(Good Bad Neutral) }
See the Rails Documentation for more usage and example of inclusion.
Update
As the Rails built-in validator does not fit your requirement, you can add a custom validator in your model like following:
validate :correct_sphare_types
private
def correct_sphare_types
if self.sphare.blank?
errors.add(:sphare, "sphare is blank/invalid")
elsif self.sphare.detect { |s| !(%w(Good Bad Neutral).include? s) }
errors.add(:sphare, "sphare is invalid")
end
end
You can implement your own ArrayInclusionValidator:
# app/validators/array_inclusion_validator.rb
class ArrayInclusionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# your code here
record.errors.add(attribute, "#{attribute_name} is not included in the list")
end
end
In the model it looks like this:
# app/models/model.rb
class YourModel < ApplicationRecord
ALLOWED_TYPES = %w[one two three]
validates :type_of_anything, array_inclusion: { in: ALLOWED_TYPES }
end
Examples can be found here:
https://github.com/sciencehistory/kithe/blob/master/app/validators/array_inclusion_validator.rb
https://gist.github.com/bbugh/fadf8c65b7f4d3eaa55e64acfc563ab2
Related
In a Rails 6 app, I have the Course model with 3 attributes: instructor_id, year, and tag.
If there a way to make a course with a tag invalid if there isn't an already defined course having the same value as year for the same instructor_id?
For instance, with the following courses table.
|instructor_id|year|tag|
|1 |2019| |
The following statements are all correct
Course.new(instructor_id: 1, tag: 2019).valid? #=> true
Course.new(instructor_id: 1, tag: 2020).valid? #=> false
Or should I write a custom validator for that?
To clarify, here my custom validator
class Course < ApplicationRecord
validates :tag, referential_integrity: { reference: :year, scope: :instructor },
allow_nil: true
end
class ReferentialIntegrityValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, :required) unless record.class.find_by(options[:scope] => record.send(options[:scope]),
options[:reference] => value)
end
end
Can I achieve the same suing built-in validation?
Maybe you are looking for a validation like this or similar with year instead of tag
validates :instructor_id, uniqueness: { scope: [:tag] }
When using the globalize gem with Active Record enums, I get an error, as if globalize doesn't know that the enum exists.
class Stuff < ActiveRecord::Base
enum stuff_type: { one: 1, two: 2 }
translates :name
validates :name, presence: true, uniqueness { case_sensitive: false, scope: :stuff_type }
default_scope do
includes(:translations)
end
end
If I do:
s = Stuff.new(name: 'stuff')
s.one!
I get an error as the following:
ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: invalid input syntax for integer: "one"
This happens because of the validation, cause it seems like globalize doesn't understand the enum.
Am I doing something wrong? How should I accomplish this?
The solution was to create my own validation method!
Something like:
validate :name_by_type
def name_by_type
max_occurrences = persisted? ? 1 : 0
occurrences = Stuff.where(stuff_type: stuff_type, name: name).count
errors['name'] << 'Name already in use' if occurrences > max_occurrences
end
Basically, I have a model, Degree, and it has three attributes: degree_type, awarded_by, and date_awarded.
There are two arrays of values that should be valid for awarded_by. The two valid values for degree_type are "one" and "two", and the valid values for awarded_by depend on "one" and "two".
If degree_type is "one" (has a value of "one", that a user would put in), I want the valid values for awarded_by to be array_one. If degree_type has a value of "two", I want the valid values for awarded_by to be array_two.
Here is the code so far:
class Degree < ActiveRecord::Base
extend School
validates :degree_type, presence: true,
inclusion: { in: ["one",
"two"],
message: "is not a valid degree type"
}
validates :awarded_by, presence: true,
inclusion: { in: Degree.schools(awarded_by_type) }
end
Degree.schools outputs an array depending on the degree type, so Degree.schools("one") would return array_one, where
array_one = ['school01', 'school02'...]
My problem is, I don't know how to access the value of degree_type within the model.
What I tried below doesn't work:
validates :awarded_by, presence: true,
inclusion: { in: Degree.schools(:degree_type) }
I tried using before_type_cast but I was either using it incorrectly or there was another problem, as I couldn't get that to work either.
When I test this I get:
An object with the method #include? or a proc, lambda or symbol is required, and must be supplied as the :in (or :within) option of the configuration hash
Help me out? :) If any more info is needed, let me know.
EDIT: To add to this, I double checked it wasn't my Degree.schools method acting up - if I go into the rails console and try Degree.schools("one") or Degree.schools("two") I do get the array I should get. :)
EDIT again: When I tried #Jordan's answer, I got errors in the cases where the awarded_by was incorrect because in those cases, valid_awarded_by_values was nil and there is no include? method for a nil object. Therefore I added an if statement checking for whether valid_awarded_by_values was nil or not (so as to return if it was), and that solved the problem!
I put this inside the method, before the unless statement and after the valid_awarded_by_values declaration:
if valid_awarded_by_values.nil?
error_msg = "is not a valid awarded_by"
errors.add(:awarded_by, error_msg)
return
end
The easiest way will be to write a custom validation method, as described in the Active Record Validations Rails Guide.
In your case, it might look something like this:
class Degree < ActiveRecord::Base
validate :validate_awarded_by_inclusion_dependent_on_degree_type
# ...
def validate_awarded_by_inclusion_dependent_on_degree_type
valid_awarded_by_values = Degree.schools(degree_type)
unless valid_awarded_by_values.include?(awarded_by)
error_msg = "must be " << valid_awarded_by_values.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')
errors.add(:awarded_by, error_msg)
end
end
end
I'm new to RoR. I'm facing a problem when using validates_uniqueness_of. I've a table with 3 columns:
name || father_name || dob
Vimal Raj || Selvam || 1985-08-30
I've a code in my model like this:
class Candidate < ActiveRecord::Base
attr_accessible :dob, :father_name, :name
validates_uniqueness_of :name, scope: [:father_name, :dob], case_sensitive: false,
message: ": %{value} already present in the database!!!"
before_save :capitalize_name, :capitalize_father_name
private
def capitalize_name
self.name.capitalize!
end
def capitalize_father_name
self.father_name.capitalize!
end
end
It throws error as expected when I insert => "vimal raj, Selvam, 1985-08-30"
But it is accepting the following data => "Vimal Raj, selvam, 1985-08-30" . I was expecting it will throw an error, but unexpectedly it accepts the record and inserts into the db as a new record.
Please help me on how to solve this.
If you want a one-liner solution, please try this :
before_validation lambda {self.name.capitalize!; self.father_name.capitalize!}
Hope, it will help.
I think the case_sensitivity is only matching on name, not on father_name. I would try changing before_save to before_validation so that both name and father_name are consistently the same capitalization when your validation is evaluated.
I'm answering my own questions - just putting this up here for google-fu in case it helps someone else. This code allows you to validate the presence of one field in a list. See comments in code for usage. Just paste this into lib/custom_validations.rb and add require 'custom_validations' to your environment.rb
#good post on how to do stuff like this http://www.marklunds.com/articles/one/312
module ActiveRecord
module Validations
module ClassMethods
# Use to check for this, that or those was entered... example:
# :validates_presence_of_at_least_one_field :last_name, :company_name - would require either last_name or company_name to be filled in
# also works with arrays
# :validates_presence_of_at_least_one_field :email, [:name, :address, :city, :state] - would require email or a mailing type address
def validates_presence_of_at_least_one_field(*attr_names)
msg = attr_names.collect {|a| a.is_a?(Array) ? " ( #{a.join(", ")} ) " : a.to_s}.join(", ") +
"can't all be blank. At least one field (set) must be filled in."
configuration = {
:on => :save,
:message => msg }
configuration.update(attr_names.extract_options!)
send(validation_method(configuration[:on]), configuration) do |record|
found = false
attr_names.each do |a|
a = [a] unless a.is_a?(Array)
found = true
a.each do |attr|
value = record.respond_to?(attr.to_s) ? record.send(attr.to_s) : record[attr.to_s]
found = !value.blank?
end
break if found
end
record.errors.add_to_base(configuration[:message]) unless found
end
end
end
end
end
This works for me in Rails 3, although I'm only validating whether one or the other field is present:
validates :last_name, :presence => {unless => Proc.new { |a| a.company_name.present? }, :message => "You must enter a last name, company name, or both"}
That will only validate presence of last_name if company name is blank. You only need the one because both will be blank in the error condition, so to have a validator on company_name as well is redundant. The only annoying thing is that it spits out the column name before the message, and I used the answer from this question regarding Humanized Attributes to get around it (just setting the last_name humanized attribute to ""