Rails validate uniqueness only if conditional
Asked Answered
W

3

36

I have a Question class:

class Question < ActiveRecord::Base
  attr_accessible :user_id, :created_on

  validates_uniqueness_of :created_on, :scope => :user_id
end

A given user can only create a single question per day, so I want to force uniqueness in the database via a unique index and the Question class via validates_uniqueness_of.

The trouble I'm running into is that I only want that constraint for non-admin users. So admins can create as many questions per day as they want. Any ideas for how to achieve that elegantly?

Wier answered 3/1, 2014 at 23:54 Comment(0)
A
92

You can make a validation conditional by passing either a simple string of Ruby to be executed, a Proc, or a method name as a symbol as a value to either :if or :unless in the options for your validation. Here are some examples:

Prior to Rails version 5.2 you could pass a string:

# using a string:
validates :name, uniqueness: true, if: 'name.present?'

From 5.2 onwards, strings are no longer supported, leaving you the following options:

# using a Proc:
validates :email, presence: true, if: Proc.new { |user| user.approved? }

# using a Lambda (a type of proc ... and a good replacement for deprecated strings):
validates :email, presence: true, if: -> { name.present? }

# using a symbol to call a method:
validates :address, presence: true, if: :some_complex_condition

def some_complex_condition
  true # do your checking and return true or false
end

In your case, you could do something like this:

class Question < ActiveRecord::Base
  attr_accessible :user_id, :created_on

  validates_uniqueness_of :created_on, :scope => :user_id, unless: Proc.new { |question| question.user.is_admin? }
end

Have a look at the conditional validation section on the rails guides for more details: http://edgeguides.rubyonrails.org/active_record_validations.html#conditional-validation

Amharic answered 4/1, 2014 at 0:20 Comment(4)
Passing a string to if condition is not supported anymore.Lofty
Thanks for the reminder - will update the answer with details.Amharic
Could also be validates :email, uniqueness: true, if: :name?Spicate
Using symbols to call a method are already mentioned in my answer.Amharic
M
5

The only way I know of to guarantee uniqueness is through the database (e.g. a unique index). All Rails-only based approaches involve race conditions. Given your constraints, I would think the easiest thing would be to establish a separate, uniquely indexed column containing a combination of the day and user id which you'd leave null for admins.

As for validates_uniqueness_of, you can restrict validation to non-admins through use of an if or unless option, as discussed in http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_uniqueness_of

Mohsen answered 4/1, 2014 at 0:7 Comment(0)
P
2

Just add a condition to the validates_uniqueness_of call.

validates_uniqueness_of :created_on, scope: :user_id, unless: :has_posted?
def has_posted
  exists.where(user_id: user_id).where("created_at >= ?", Time.zone.now.beginning_of_day)
end

But even better, just create a custom validation:

validate :has_not_posted
def has_not_posted
  posted = exists.where(user: user).where("DATE(created_at) = DATE(?)", Time.now)
  errors.add(:base, "Error message") if posted
end
Parrnell answered 4/1, 2014 at 0:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.