ActiveRecord: user-specific validations - ruby-on-rails

I want to create user-specific validations.
User has a column called "rule_values" which is a serialized hash of certain quantities.
In a separate model, Foo, I have a validation:
class Foo < ActiveRecord::Base
belongs_to :user
n = self.user.rule_values[:max_awesome_rating] #this line is giving me trouble!
validates_presence_of :awesome_rating, :in => 1..n
end
It seems that self refers to Foo (which is why I'm getting an undefined method error) and not an instance of Foo. How can I access the User instance from within the Foo model?

How about creating a custom validation on Foo something like this?
class Foo < ActiveRecord::Base
validate do |foo|
n = foo.user.rule_values[:max_awesome_rating]
unless (1..n).include? foo.awesome_rating
foo.errors.add :awesome_rating, "must be present and be between 1 and #{n}"
end
end
end
This way you have access to the instance and the user association

Rails supports custom validations (using validate). Here's an idea of how it might work (did not check it though):
class Foo < ActiveRecord::Base
belongs_to :user
validate :awesome_rating_is_in_range
def awesome_rating_is_in_range
errors.add(:awesome_rating, 'not in range') unless (1..user.rule_values[:max_awesome_rating]).include? awesome_rating
end
end

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

Validate children before creating parent

How can I create a parent model only if its children models fit some validations alltogether like the sum of their attributes equal come value, etc?
I have an parent model as:
class Foo < ApplicationRecord
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars, allow_destroy: true
end
And the child model as:
class Bar < ApplicationRecord
belongs_to :foo
end
Is there a right way of doing it? Where should I validate the children models? Also how could I test it with rspec? Like this?
before do
#foo = create(:foo)
#bar = create(:bar, value: 30, foo_id: #foo.id)
end
Rails offers validates_associated, which will ensure the associated records are valid. If the associated records are invalid, the parent record will not be saved.
In Foo:
class Foo < ApplicationRecord
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars, allow_destroy: true
validates_associated :bars
end
In Bar:
class Bar < ApplicationRecord
belongs_to :foo
include ActiveModel::Validations
validates_with BarValidator
end
In BarValidator, which is a custom validator:
class BarValidator < ActiveModel::Validator
def validate(record)
record.errors.add :some_error, 'Some message' unless condition_met?
end
end
You've stated:
only if its children models fit some validations alltogether like the sum of their attributes equal come value, etc?
which is somewhat ambiguous. If you truly do need to calculate the sum of children then you can add a validator to the parent which maps through the children and appends an error on failure to meet conditional:
In Foo, (or preferably, a validator):
validate :children_condition
def children_condition
errors[:base] << "Some message" if bars.map(&:attribute).sum != expected_minimum_value
end
Important notes from the documentation on validates_associated:
WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion.
NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of.
As for:
Also how could I test it with rspec?
I would create a test suite of valid parent, valid child && valid parent, invalid child, etc and expect the Model.count to have increased by the expected amount (which would be zero in the latter example).

Prevent parent object to be saved when saving child object fails

I have two associated classes like this:
class Purchase < ActiveRecord::Base
has_many :actions
before_create do |p|
self.actions.build
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
The block in the Action class prevents it from saving. I was thinking doing Purchase.create will fail because it cannot save the child object. But while it does not save the Action, it commits the Purchase. How can i prevent the parent object to be saved when there is an error in the child object?
It turns out you have to rollback the transaction explicitly, errors from the child objects does not propagate. So i ended up with:
class Purchase < ActiveRecord::Base
has_many :actions
after_create do |p|
a = Action.new(purchase: p)
if !a.save
raise ActiveRecord::Rollback
end
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
Take note that i also changed the before_create callback to after_create. Otherwise, since belongs_to also causes the parent to be saved, you will get a SystemStackError: stack level too deep.
I ran into this problem when dealing with race conditions where the child objects would pass a uniqueness validation, but then fail the database constraint (when trying to save the parent object), leading to childless (invalid) parent objects in the database.
A slightly more general solution to the one suggested by #lunr:
class Purchase < ActiveRecord::Base
has_many :actions
after_save do
actions.each do |action|
raise ActiveRecord::Rollback unless action.save
end
end
end
class Action < ActiveRecord::Base
belongs_to :purchase
before_save do |a|
false
end
end
Try to use this code in Purchase class:
validate :all_children_are_valid
def all_children_are_valid
self.actions.each do |action|
unless action.valid?
self.errors.add(:actions, "aren't valid")
break
end
end
end
Or use validates_associated in Purchase class:
validates_associated :actions
If in your business logic you can't save purchase without any action, then add a presence validator on actions inside purchases
validates :actions, length: {minimum: 1}, presence: true

validate the presence of at least one association object in rails 3.2

I've one small problem, I can't get solved. I want to validate that there is at least one associated model. Like in the following
class User < ActiveRecord::Base
has_many :things
validates_presence_of :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
This works fine when I update my model via #update_attributes, but when I simply set #user.things = [], I am able to get invalid data in the database. My workaroud to solve this is to overwrite the setter method
def things=(val)
begin
if val.blank?
errors.add(:things, "not valid")
raise SomeError
end
super
rescue SomeError
false
end
end
But somehow this doesn't feel right. Isn't there a way to archive the same result via validations and/or callbacks, preferably so that #things= return false (and not val) and so that #user.things is not changed (I mean the cached #user.things, #user.things(true) should work fine anyway).
You can create a custom validator that will check the presence of things.
Instead of
validates_presence_of :things
You could do
validate :user_has_things
def user_has_things
if self.things.size == 0
errors.add("user has no thingies")
end
end

ActiveRecord Problems using callbacks and STI

Hey folks, following problem with Rails and STI:
I have following classes:
class Account < AC::Base
has_many :users
end
class User < AC::Base
extend STI
belongs_to :account
class Standard < User
before_save :some_callback
end
class Other < User
end
end
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
type.new(*args, &block)
end
end
end
And now the problem:
Without rewriting User.new (in module STI), the callback inside User::Standard gets never called, otherwise the account_id is always nil if I create users this way:
account.users.create([{ :type => 'User::Standard', :firstname => ... }, { :type => 'User::Other', :firstname => ... }])
If I'm using a different approach for the module like:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
Then instance variables are not shared, because it's creating a new object.
Is there any solution for this problem without moving the callbacks to the parent class and checking the type of class?
Greetz
Mario
Maybe there's something I don't know, but I've never seen Rails STI classes defined in that manner. Normally it looks like...
app/models/user.rb:
class User < AC::Base
belongs_to :account
end
app/models/users/standard.rb:
module Users
class Standard < User
before_save :some_callback
end
end
app/models/users/other.rb:
module Users
class Other < User
end
end
It looks as though you are conflating class scope (where a class "lives" in relation to other classes, modules, methods, etc.) with class inheritance (denoted by "class Standard < User"). Rails STI relationships involve inheritance but do not care about scope. Perhaps you are trying to accomplish something very specific by nesting inherited classes and I am just missing it. But if not, it's possible it's causing some of your issues.
Now moving on to the callbacks specifically. The callback in Standard isn't getting called because the "account.users" relationship is using the User class, not the Standard class (but I think you already know that). There are several ways to deal with this (I will be using my class structure in the examples):
One:
class Account
has_many :users, :class_name => Users::Standard.name
end
This will force all account.users to use the Standard class. If you need the possibility of Other users, then...
Two:
class Account
has_many :users # Use this to look up any user
has_many :standard_users, :class_name => Users::Standard.name # Use this to look up/create only Standards
has_many :other_users, :class_name => Users::Other.name # Use this to look up/create only Others
end
Three:
Just call Users::Standard.create() and Users::Other.create() manually in your code.
I'm sure there are lots of other ways to accomplish this, but there are probably the simplest.
So I solved my problems after moving my instance variables to #attributes and using my second approach for the module STI:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
class User < AR:Base
extend STI
belongs_to :account
validates :password, :presence => true, :length => 8..40
validates :password_digest, :presence => true
def password=(password)
#attributes['password'] = password
self.password_digest = BCrypt::Password.create(password)
end
def password
#attributes['password']
end
class Standard < User
after_save :some_callback
end
end
Now my instance variable (the password) is copied to the new User::Standard object and callbacks and validations are working. Nice! But it's a workaround, not really a fix. ;)

Resources