Pass validation context to associated model - ruby-on-rails

I'm using contexts to invoke specific validations at different points in the model lifecycle:
model Address
validates :city, presence: true
validates :street, presence: true, on: :send_letter
end
incomplete_address = Address.new(city: 'Berlin')
incomplete_address.valid? # => true
incomplete_address.valid?(:send_letter) # => false
This works fine for the simple case above. But, as far as I can tell, the context is ignored for any associated objects:
model Address
belongs_to :country
validates :street, presence: true, on: :send_letter
validates_associated :country
end
model Country
has_many :addresses
validates :iso_alpha_3, presence: true, size: 3, on: :send_letter
end
incomplete_address = Address.new(street: 'Oranienstr', country: Country.new(name: 'Germany', iso_alpha_3: 'Invalid iso code')
incomplete_address.valid? # => true
incomplete_address.valid?(:send_letter) # => true
incomplete_address.country.valid?(:send_letter) => false
Question: Is this expected behaviour, or is it a bug I'm hitting? Or am I making a conceptual mistake? What's the most elegant way to validate associated models under such circumstances?

I know this question is 3 years old but there is a slightly easier path now and an an even easier option on the horizon.
There is currently an outstanding PR that adds this functionality via a configuration option on the validates_associated call. In the meantime, you can add that version of AssociatedValidator as a separate validator (e.g. AssociatedPreservingContextValidator) and call validate_with AssociatedPreservingContextValidator, :country in `Address.

Is the expected behaviour, validators are executed only in the current model.
You can achieve the desired result using a custom method validator.
You can find more informations here

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?}

Correct callbacks for geocoder validation

I hope this is not a dupe but since I haven't found a satisfying answer so far I'm gonna put my question. I'm a rails beginner so please be gentle on me ;)
For a small mapping project we're using geocoder gem. We have a model Place containing :street, :house_number, :postal_code, :city wrapped up as a simple string by :address as the virtual attribute which is being passed to geocoder. We're doing the querying using nominatim (-> OSM data). Unlike the google API nominatim returns nil if the given address does not correspond to a lat/lon pair. So far our model file looks like this
class Place < ActiveRecord::Base
has_many :descriptions, :dependent => :delete_all
accepts_nested_attributes_for :descriptions
geocoded_by :address
before_validation :geocode
validates :address, presence: true
validates :name, presence: true
validates :longitude, presence: true,
numericality: { less_than_or_equal_to: 90, greater_than_or_equal_to: -90 }
validates :latitude, presence: true,
numericality: { less_than_or_equal_to: 180, greater_than_or_equal_to: -180 }
validates :categories, presence: true
def address
["#{self.street} #{self.house_number}", self.postal_code, self.city].join(', ')
end
end
Unlike most of the examples I've looked at I'm using before_validation :geocode because otherwise lat and lon would be nil before they become evaluated raising a validation error (not present).
So far this works for creating new places but I'm unable to modify the validation to make it work for the update action inside my controller. Basically I want to check if the changes applied to address are valid (i.e. whether the API finds a lat/lon pair to the given address). Studying the documentation i found, that i do a conditional callback like before_validate :geocode, if: :address_changed? but since the parameters of the update request are not available inside my model I have no clue how to check that. I'd be glad on any hint!
edit:
Accepted answer below is right, wasn't working instantly because inside my controller I was using update instead of update_attributes.
Your model doesn't need to know the parameters to update the request. You can check if the address changed by checking if any of the columns which make up your address have changed. Since address isn't a column, you'll need to define address_changed? like so:
before_validation :geocode, if: :address_changed?
def address_changed?
street_changed? || house_number_changed? || postal_code_changed? || city_changed?
end
Note that street, house_number, city and postal_code will all need to be columns in order for the above to work.

Ruby on Rails: Getting validates_uniqueness_of to work

I have two params :work and :grade. In the model, before saving I want to use validates_uniqueness_of to check given a unique work, there is only one grade. Grade can be the same for other work. How would I write this?
Edit:
validates_uniqueness_of :work, :scope => :grade
If you have a deprecated syntax warning, you can write it so:
validates :work, uniqueness: {scope: :grade}, presence: true
Edit:
It seems you need a two way checking, so perhaps adding this will work:
validates :grade, uniqueness: {scope: :work}, presence: true
Although under high load I've seen this fail, so best is to create a database constraint.

Rails validate uniqueness only if conditional

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

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