Rails validate uniqueness only if conditional - ruby-on-rails

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?

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

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

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

Related

validating attribute presence with condition

I am trying to introduce a functionality in my application in which the users can suggest an event to the organizers.To suggest an event the user will have to fill a form. I want to validate the presence of few fields(the attribute of the events) with a condition to be true, i.e. the fields should not be left blank if the user(the submitter of the form) is not an admin.However if the submitter of the form is an admin, the fields can be left blank.
validates :attribute_name, presence: { :if => :user.is_admin? }
undefined method `is_admin?' for :user:Symbol Did you mean? is_haml?
I have also tried :user.is_admin()==True , It also throws error:
undefined method `is_admin' for :user:Symbol Did you mean? is_a?
I have this attribute in the users table:
t.boolean "is_admin", default: false
I have the following associations defined in my events model:
has_many :event_users, dependent: :destroy
has_many :users, through: :event_users
has_one :submitter_event_user, -> { where(event_role: 'submitter') }, class_name: 'EventUser'
has_one :submitter, through: :submitter_event_user, source: :user
In the controllers I have this code:
#event.submitter = current_user
The issue is you are trying to call is_admin? on the Symbol :user as the error suggests.
If I understand correctly this attribute should be present unless user.is_admin? returns true
This can be done in multiple ways:
validates :attribute_name, presence: true, unless: -> {|u| u.is_admin?}
#Or
validates :attribute_name, presence: true, unless: :is_admin?
# or using presence directly as you tried to originally
validates :attribute_name, presence: {unless: -> {|u| u.is_admin?} }
# or
validates :attribute_name, presence: {unless: :is_admin? }
I generally prefer the first option as, IMO, it is the least ambiguous and the most readable but all of them should result in the same function so choose the one you prefer as long as you remain consistent.
In the block form the instance is yielded to the block and the block's return value is used to determine whether or not the validation should run.
When using the symbol the symbol is sent to the instance via the send message transmission e.g. self.send(:is_admin? and again the return value is used to determine if the validation should be applied
ActiveModel::Validations::ClassMethods#validates
Update based on revised question:
Since the Event is related to the User via submitter and this is already being set to the instance of a User you can validate in a very similar fashion via
validates :attribute_name, presence: true,
unless: ->(event) { event.submitter&.is_admin?}
Or make a separate method such as
def admin_submitter?
self.submitter&.is_admin?
end
validates :attribute_name, presence: {unless: :admin_submitter?}

validate uniqueness of polymorphic association

I'm trying to implement a validation for a polymorphic association, where I only want it to trigger on a certain type. Which is user.
I'd want something like this:
validates :room_id, uniqueness: { scope: tokenable_id if tokenable type is User }
how do I go about doing this. The other type is Customer. Which I want to allow the opportunity to be several.
I think you can use Conditional Validation
Sometimes it will make sense to validate an object only when a given
predicate is satisfied. You can do that by using the :if and :unless
options, which can take a symbol, a string, a Proc or an Array. You
may use the :if option when you want to specify when the validation
should happen. If you want to specify when the validation should not
happen, then you may use the :unless option.
5.1 Using a Symbol with :if and :unless
You can associate the :if and :unless options with a symbol
corresponding to the name of a method that will get called right
before validation happens. This is the most commonly used option.
class Order < ActiveRecord::Base
validates :room_id, uniqueness: { scope: :tokenable_id }, if: :is_right_type?
# or maybe this will work
validates :room_id, uniqueness: { scope: :tokenable_id }, if: :user?
def is_right_type?
type == "user"
end
end

How can you check if all of a model's attributes have a value set?

I currently have the following method:
def complete?
attributes.delete_if{|k,v| k == 'id' or k == 'user_id'}.values.each do |value|
return false if value.blank?
end
true
end
Is there a better way of doing this? I just want to know if, with my instance, all the attributes have been set apart from id and user_id. There must be a better way of doing this.
I'm on rails 3 and ruby 1.9.3 (just so people don't give answers that will work with newer versions)
There is the validates method to do that:
class User < ActiveRecord::Base
validates :full_name, :username, :email, :address, presence: true
If you want to validate the presence of every attributes except few ones:
class User < ActiveRecord::Base
validates *(self.column_names - ['id', 'created_at', 'updated_at']), presence: true
Above exemple extended:
class User < ActiveRecord::Base
validates *self.validable_columns, presence: true
def self.validable_columns
excluded_columns = ['id', 'created_at', 'updated_at'] # columns to be excluded in the `validates`
self.column_names - excluded_columns
end

Rails - one model, 2 kinds of validation rules

In an app I have 3 types of contact forms - in the model - the attributes :aaa, :bbb, :ccc belongs to the second contact form, the previous attributes belongs to the first contact form.
class Message
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :email, :body, :aaa, :bbb, :ccc
validates :name, :email, :body, :aaa, :bbb, :ccc, :presence => true
validates :email, :format => { :with => %r{.+#.+\..+} }, :allow_blank => true
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
What I am trying to do: I am looking for a way, how to validate attributes for the respective contact forms, specifically:
the first contact form contains attributes: :name, :email, :body, which I need to validate
the second contract form contains attributes: :aaa, :bbb, :ccc, :email, which I need to validate
How to do that? How to distinguish, which attributes belongs to which form and validate them?
If there are 3 types of contract forms why not make them 3 separate classes?
If for some erason you still want to keep it in one class, you can do it using 'with_options' magic:
with_options :if => :is_form_1? do |p|
p.validates_presence_of :attr1
p.validates_presence_of :attr2
p.validates_presence_of :attr3
end
with_options :if => :is_form_2? do |p|
p.validates_presence_of :attr4
p.validates_presence_of :attr5
p.validates_presence_of :attr6
end
def is_form_1?
#some logic
end
def is_form_2?
#some logic
end
Still, I don't like the idea of keeping it in one class.
I'd suggest you think about this in behavioural rather than implementation terms. You mention there are three contact forms, but what is the underlying use that you're putting each one to? You shouldn't be thinking about forms when you're setting up your model.
That having been said, you can achieve what you want using the validation_scopes gem. Using validation_scopes, you can define sets of validation rules that you can treat independently. In your controllers, you can then check whichever set of validation rules apply to the context (i.e. which form the user has filled in).
In your model you can set up validation scopes named for each form (or better, named for the context in a way that has semantic value, but I don't know enough about your app to know what the contexts are), like this:
validation_scope :form_one_errors do |vs|
validates :name, :body, :presence => true
end
validation_scope :form_two_errors do |vs|
validates :aaa, :bbb, :ccc, :presence => true
end
Since email needs to be validated in both contexts, you can just set it up as a normal validation (as per your code in the question).
Then in the controller for, say, form one, you can check the scope to see if there are any errors for that context. Note that you have to check the errors for the validation scope separately for the regular validation errors.
if !message.valid?
# Do something with message.errors
elsif message.has_form_one_errors?
# Do something with message.form_one_errors
else
# All good
end

Rails - force field uppercase and validate uniquely

Airports have four-letter ICAO codes. By convention, these are always uppercase. I'm creating a form for receiving user input, but this form needs to be able to accept user input in mixed case, and prevent them from creating dupes.
The default :uniqueness is case-sensitive, of course. I figured out how to transform the user's input to uppercase before it gets saved, but the problem is that this appears to be post-validation, instead of pre-validation.
For example, if there is already an Airport with ICAO of KLAX, a user can enter klax, it will get validated as unique, and then transformed to uppercase and stored, resulting in duplicates.
Here's my model code at present.
class Airport < ActiveRecord::Base
validates :icao, :name, :lat, :lon, :presence => true
validates :icao, :uniqueness => true
before_save :uppercase_icao
def uppercase_icao
icao.upcase!
end
end
Or a slightly different take: Write a setter for icao that converts anything thrown at it to uppercase:
def icao=(val)
self[:icao] = val.upcase
end
And then you can use regular uniqueness validation (back it up with a unique index in your DB). Might even make things a little easier for the DB during finds, 'cause it doesn't have to worry about case-insensitive comparisons any more.
Hope this helps!
try this:
validates :icao, :uniqueness => { :case_sensitive => false }
Updated answer for Rails 4.
class Airport < ActiveRecord::Base
validates :icao, :name, :lat, :lon, :presence => true
validates :icao, :uniqueness => { case_sensitive: false }
def icao=(val)
write_attribute :icao, val.upcase
end
end
Simply fixed (as many problems with Rails are) - as Danny pointed out above, although not in his own answer so I can't accept it :), changing before_save to before_validation fixes it perfectly.

Resources