My models are
class Company
has_many :admins
validate :has_one_admin_validation
private
def has_one_admin_validation
errors.add(:admins, :not_enough) if admins.size < 1
end
end
class Admin
belong_to :company
end
Now, suppose I have a controller that can remove admins. How do I prevent removing the admin (ie generate errors) if it is the only admin of its company ?
If I understand well, I have to remove the admin from the memory object, and try to "save/destroy" if by validating the company first ?
I don't think you need a custom validation at all on the Company model. You can use the 'length' validation on your association.
validates :admins, length: { minimum: 1 }
If that doesn't work, you should also be able to check the 'marked_for_destruction?' property. You should also be able to validate the reciprocal relationship with a 'presence: true' validation.
class Company
has_many :admins
validate :has_one_admin_validation
private
def has_one_admin_validation
errors.add :admins, "You need at least one admin" if admins.reject(&:marked_for_destruction?).empty?
end
end
class Admin
belongs_to :company, presence: true
end
You may also want to look at using the before_destroy callback in your Admin class.
before_destroy :has_company_with_no_other_admins
...
private
def has_company_with_no_other_admins
return false if company.admins.length < 2
true
end
There's a pretty good description of using before_destroy here: https://stackoverflow.com/a/123190/6441528
That's worth looking at because implementations vary based on your Rails version.
Related
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
If you are saving a has_many :through association at record creation time, how can you make sure the association has unique objects. Unique is defined by a custom set of attributes.
Considering:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
What is the best way to validate the user_roles and roles are unique based on arbitrary conditions on an association?
The best way to do this, especially if you're using a relational db, is to create a unique multi-column index on user_roles.
add_index :user_roles, [:user_id, :role_id], unique: true
And then gracefully handle when the role addition fails:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
Relational DBs are designed to guarantee referential integrity, so use it for exactly that. Any ruby/rails-only solution will have race conditions and/or be really inefficient.
If you want to provide user-friendly messaging and check "just in case", just go ahead and check:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
You'll still have to handle the potential exception when you try to persist role addition, though.
Just do a multi-field validation. Something like:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
Something like that will ensure that a user can have only one role for a given project - if that's what you're looking for.
As mentioned by Kache, you probably also want to do a db-level index. The whole migration might look something like:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
The name: argument is optional but can be handy in case the concatenation of the field names gets too long (and throws an error).
I have 2 models: Dealer & Location.
class Dealer < AR::Base
has_many :locations
accepts_nested_attributes_for :locations
validate :should_has_one_default_location
private
def should_has_one_default_location
if locations.where(default: true).count != 0
errors.add(:base, "Should has exactly one default location")
end
end
end
class Location < AR::Base
# boolean attribute :default
belongs_to :dealer
end
As you understood, should_has_one_location adds error everytime, because .where(default: true) makes an sql query. How can I avoid this behaviour?
The very dirty solution is to use combination of inverse_of and select instead of where, but it seems very dirty. Any ideas?
I actually got an answer to a similar question of my own. For whatever it's worth, If you wanted to do a validation like you have above (but without the db query), you would do the following:
errors.add(:base, ""Should have exactly one default location") unless locations.any?{|location| location.default == 'true'}
I have this structure
class Organization
has_many :clients
end
class Client
belongs_to :organization
has_many :contacts
end
class Contact
belongs_to :client
belongs_to :organization
end
How can I make sure that when client is assigned to a contact he is a child of a specific organization and not allow a client from another organization to be assigned ?
While searching I did find a scope parameter can be added but that seems not to be evaluated when client_id is assigned.
Update
Here is an example from Rails Docs :
validates :name, uniqueness: { scope: :year,message: "should happen once per year" }
I'm looking for something like "if client is set it must be in Organization.clients"
class Contact
#...
validate :client_organization
def client_organization
unless client.nil?
unless organization == client.organization
errors.add(:organization, "can't be different for client.")
end
end
end
end
I'm having a potluck where my friends are coming over and will be bringing one or more food items. I have a friend model and each friend has_many food_items. However I don't want any two friends to bring the same food_item so food_item has to have a validations of being unique. Also I don't want a friend to come (be created) unless they bring a food_item.
I figure the best place to conduct all of this will be in the friend model. Which looks like this:
has_many :food_items
before_create :make_food_item
def make_food_item
params = { "food_item" => food_item }
self.food_items.create(params)
end
And the only config I have in the food_item model is:
belongs_to :friend
validates_uniqueness_of :food_item
I forsee many problems with this but rails is telling me the following error: You cannot call create unless the parent is saved
So how do I create two models at the same time with validations being checked so that if the food_item isn't unique the error will report properly to the form view?
How about to use nested_attributes_for?
class Friend < ActiveRecord::Base
has_many :food_items
validates :food_items, :presence => true
accepts_nested_attributes_for :food_items, allow_destroy: true
end
You're getting the error because the Friend model hasn't been created yet since you're inside the before_create callback. Since the Friend model hasn't been created, you can't create the associated FoodItem model. So that's why you're getting the error.
Here are two suggestions of what you can do to achieve what you want:
1) Use a after_create call back (I wouldn't suggest this since you can't pass params to callbacks)
Instead of the before_create you can use the after_create callback instead. Here's an example of what you could do:
class Friend
after_create :make_food_item
def make_food_item
food_params = # callbacks can't really take parameters so you shouldn't really do this
food = FoodItem.create food_params
if food.valid?
food_items << food
else
destroy
end
end
end
2) Handle the logic creation in the controller's create route (probably best option)
In your controller's route do the same check for your food item, and if it's valid (meaning it passed the uniqueness test), then create the Friend model and associate the two. Here is what you might do:
def create
friend_params = params['friend']
food_params = params['food']
food = FoodItem.create food_params
if food.valid?
Friend.create(friend_params).food_items << food
end
end
Hope that helps.
As mentioned, you'll be be best using accepts_nested_attributes_for:
accepts_nested_attributes_for :food_items, allow_destroy: true, reject_if: reject_if: proc { |attributes| attributes['foot_item'].blank? }
This will create a friend, and not pass the foot_item unless one is defined. If you don't want a friend to be created, you should do something like this:
#app/models/food_item.rb
Class FootItem < ActiveRecord::Base
validates :[[attribute]], presence: { message: "Your Friend Needs To Bring Food Items!" }
end
On exception, this will not create the friend, and will show the error message instead