How to perform an inclusion validation on a serialized attribute? - ruby-on-rails

I have a model with an serialized attribute (array). I would like to validate the model only if each member of the array is included within the pre-determined options.
Example:
I have a Person model which has a "mood" attribute. Users can have more than one mood, but each mood must be either 'happy', 'sad', 'tired' or 'angry'.
The model would be something like this:
class Person < ActiveRecord::Base
MOODS = %w[happy sad tired angry]
# validates :inclusion => { :in => MOODS }
attr_accessible :mood
serialize :mood
end
The commented validation doesn't work. Is there any way to make it work or do I need a custom validation?
(Note: I don't want to create a separate Mood model.)

class Person < ActiveRecord::Base
MOODS = %w[happy sad tired angry]
validate :mood_check
attr_accessible :mood
serialize :mood
protected
def mood_check
mood.each do |m|
errors.add(:mood, "#{m} is no a valid mood") unless MOODS.include? m
end
end
end

Leaving this here in case it helps anyone in the future - I've written a gem to better handle validating serialized attributes. You can just put those validations in a block syntax, the ways you might expect to:
class Person < ActiveRecord::Base
MOODS = %w[happy sad tired angry]
attr_accessible :mood
serialize :mood
validates_array_values :mood, inclusion: { in: MOODS }
end
https://github.com/brycesenz/validates_serialized

Related

Rails: querying associations during validation

During validation, I want to query associations but neither solution seems to be good because ActiveRecord’s style of validation. Here is an example:
class User < ApplicationRecord
has_many :borrowed_books
validate :should_only_borrow_good_books
def should_only_borrow_good_books
# What I want but it does not work:
#
# unless borrowed_books.where(condition: "bad").empty?
# errors.add(:borrowed_books, "only good books can be borrowed")
# end
#
# ^ this query always returns an empty array
# This approach works but it's not ideal:
unless borrowed_books.all? { |b| b.condition == "good" }
errors.add(:borrowed_books, "only good books can be borrowed")
end
end
end
class BorrowedBook < ApplicationRecord
belongs_to :user
# attr: condition - ["bad", "good"]
end
One more option is to move the validation to BorrowedBook with something like validates :condition, inclusion: { in: %w(good) }, if: -> { user_id.present? } and perhaps validate association in User like validates_associated :borrowed_books. But I don't like this approach because it complicates things by moving the logic belonging to User to BorrowedBook. A few validations like this and your app might become really messy.
This validation should definitely stay in the user model. I do disagree that it will look messy if there are multiple conditions. If it makes messy, it often indicate that you should split the model or refactor the code there. It's the models job to enable you to access and validate the data from db. A way to improve the current code is to convert the condition column to enum, ref: https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Enum.html you can write it like this
class User < ApplicationRecord
has_many :borrowed_books
validate :should_only_borrow_good_books
private
def should_only_borrow_good_books
return unless books.not_good.any?
errors.add(:borrowed_books, "only good books can be borrowed")
end
end
end
class BorrowedBook < ApplicationRecord
belongs_to :user
enum status: [ :good, :bad ]
end

Raise validation error from virtual attribute in Rails

I got two models: Source and SourceType. Source of course belongs to SourceType.
I want to create new source and assign proper sourcetype object to it. 'Proper' means that one virtual attribute of the source object match some of sourceType objects test regexpression, which becomes source's Type.
I got an attribute writer in source object
class Source < ActiveRecord::Base
belongs_to :source_type
def url=(value)
SourceType.each do |type|
# here i match type's regexp to input value and if match,
# assign it to the new source object
end
end
end
I don't want to build any custom validator for it'll be need to run through SourceTypes twice. How to raise validate error if no sourcetypes is fit to the input so user could see error reasons in a form?
Validation
If you set the virtual attribute using attr_accessor, you should be able to validate on the model you're sending data to (alternatively using inverse_of if you'd like to validate on the nested model):
http://api.rubyonrails.org/classes/ActiveModel/Validator.html
This can now be used in combination with the validates method (see ActiveModel::Validations::ClassMethods.validates for more on this).
class Person
include ActiveModel::Validations
attr_accessor :title
validates :title, presence: true
end
Code
I'd do this:
class Source < ActiveRecord::Base
belongs_to :source_type, inverse_of: :sources
attr_accessor :url
end
class SourceType < ActiveRecord::Base
has_many :sources, inverse_of: :source_type
validates :source_type_attr, presence: { if: :url_match? }
def url_match?
self.sources.url == [your_regex]
end
end

Editing a has_one association in ActiveAdmin - avoid saving when nothing is entered

I've got a model in which a very small percentage of the objects will have a rather large descriptive text. Trying to keep my database somewhat normalized, I wanted to extract this descriptive text to a separate model, but I'm having trouble creating a sensible workflow in ActiveAdmin.
My models look like this:
class Person < ActiveRecord::Base
has_one :long_description
end
class LongDescription < ActiveRecord::Base
attr_accessible :text, :person_id
belongs_to :person
validates :text, presence: true
end
Currently I've created a form for editing the Person model, looking somewhat like this:
form do |f|
...
f.inputs :for => [
:long_description,
f.object.long_description || LongDescription.new
] do |ld_f|
ld_f.input :text
end
f.actions
end
This works for adding/editing the LongDescription object, but I still have an issue: I'd like to avoid validating/creating the LongDescription object if no text is entered.
Anyone with better ActiveAdmin skills than me know how to achieve this?
Are you using accepts_nested_attributes_for :long_description? If so, you can add a :reject_if option:
class Person < ActiveRecord::Base
has_one :long_description
accepts_nested_attributes_for :long_description, reject_if: proc { |attrs| attrs['text'].blank? }
end
Note that this is a Rails thing, not an ActiveAdmin thing, and so it will simply skip assignment and update/create of the nested object if that attribute is missing.
More here: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Editing a has_one association in ActiveAdmin - destroying when attribute is blanked

I've got a model in which a very small percentage of the objects will have a rather large descriptive text. Trying to keep my database somewhat normalized, I wanted to extract this descriptive text to a separate model, but I'm having trouble creating a sensible workflow in ActiveAdmin.
My models look like this:
class Person < ActiveRecord::Base
has_one :long_description
accepts_nested_attributes_for :long_description, reject_if: proc { |attrs| attrs['text'].blank? }
end
class LongDescription < ActiveRecord::Base
attr_accessible :text, :person_id
belongs_to :person
validates :text, presence: true
end
Currently I've created a form for editing the Person model, looking somewhat like this:
form do |f|
...
f.inputs :for => [
:long_description,
f.object.long_description || LongDescription.new
] do |ld_f|
ld_f.input :text
end
f.actions
end
This works for adding/editing the LongDescription object, and it avdois validating/creating the LongDescription object if no text is entered.
What I'd like to achieve is to also be able to remove the LongDescription object, for example if the text attribute is ever set to an empty string/nil again.
Anyone with better Rails or ActiveAdmin skills than me know how to achieve this?
That seems like an awfully unusual architecture decision, but the implementation is pretty simple:
class LongDescription < ActiveRecord::Base
validates_presence_of :text, on: :create
after_save do
destroy if text.blank?
end
end

Rails setting OR conditions in validate_presence_of in a model?

In a rails model, is it possible to do something like
class Example < ActiveRecord::Base
#associations
validates_presence_of :item_id, (:user_id OR :user_email)
#functions
end
Where the model has 3 columns of :item_id, :user_id, and :user_email?
I want the model to be valid as long as I have a :user_id or a :user_email.
Idea being that if the item is recommended to a person who isn't currently signed up, it can be associated via email address for when the recommended person signs up.
Or is there a different method that I can use instead?
One approach is to wrap those fields as a virtual attribute, say:
class Example < ActiveRecord::Base
validates_presence_of :referral
def referral
user_id || user_email
end
end
or you can just throw a custom validate validation method. See custom validations on the Rails API
If both user_id and user_email come from another model, perhaps it's better to add the association instead
class Example
belongs_to :user
validates_associated :user
before_validate :build_user_from_id_or_email
def build_user_from_id_or_email
# ... Find something with the parameters
end
end
validates_presence_of :item_id
validates_presence_of :user_id, :if => Proc.new{ |x| x.user_email.blank? }
validates_presence_of :user_email, :if => Proc.new{ |x| x.user_id.blank? }

Resources