Correct callbacks for geocoder validation - ruby-on-rails

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.

Related

Pass validation context to associated model

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

Partly invoke validation process on some Activerecord object attributes

I have a situation where User has_one :address and Address belongs_to :user.
I need to be able to validate the address object in these cases:
After a user has signed up, he has an option to partly fill in the address form. In this state I would like to validate for example validates :phone_number, :postal_code, numericality: true but the user can leave the field blank if he wants to.
When user is making a purchase he has to complete the address form. And all the fields have to be validated by validates presence: true + previous validations.
I understand that one approach would be to attach another parameter to the form (i.e.full_validation) and then add a custom validation method that would check for this parameter and then fully validate all attributes.
I was just wondering is there a more code efficient and easier way to do this.
So far I have only found ways to validate some attributes (seethis blog post) but I have not yet found suggestions on how to invoke part of the validation process for certain attributes.
Any help/suggestions will be appreciated :)
#app/models/user.rb
class User < ActiveRecord::Base
has_one :address, inverse_of: :user
end
#app/models/address.rb
class Address < ActiveRecord::Base
belongs_to :user, inverse_of: :address
validates :phone_number, :postal_code, numericality: true, if: ["phone_number.present?", "postal_code.present?"]
validates :x, :y, :z, presence: true, unless: "user.new_record?"
end
--
After a user has signed up
Use if to determine if the phone_number or postal_code are present.
This will only validate their numericality if they exist in the submitted data. Whether the User is new doesn't matter.
--
When user is making a purchase
To make a purchase, I presume a User has to have been created (otherwise he cannot purchase). I used the user.new_record? method to determine whether the user is a new record or not.
Ultimately, both my & #odaata's answers allude to the use of conditional evaluation (if / unless) to determine whether certain attributes / credentials warrant validation.
The docs cover the issue in depth; I included inverse_of because it gives you access to the associative objects (allowing you to call user.x in Address).
If you give more context on how you're managing the purchase flow, I'll be able to provide better conditional logic for it.
For your first use case, you can use the :allow_blank option on validates to allow the field to be blank, i.e. only validate the field if it is not blank?.
http://guides.rubyonrails.org/active_record_validations.html#allow-blank
For both use cases, you can tell Rails exactly when to fire the validations using the :if/:unless options. This is known as Conditional Validation:
http://guides.rubyonrails.org/active_record_validations.html#conditional-validation
For Address, you might try something like this:
class Address
belongs_to :user
validates :phone_number, :postal_code, numericality: true, allow_blank: true, if: new_user?
def new_user?
user && user.new_record?
end
end
This gives you an example for your first use case. As for the second, you'll want to use conditional validation on User to make sure an address is present when the person makes a purchase. How this is handled depends on your situation: You could set a flag on User or have that flag check some aspect of User, e.g. the presence of any purchases for a given user.
class User
has_one :address
has_many :purchases
validates :address, presence: true, if: has_purchases?
def has_purchases?
purchases.exists?
end
end

before validation do something, no method error

I have an model for notifications.
An notification can be posted by an User or an Contact.
and the notification can go to either a business or a notification_area.
people has to be filled, so when there is no user added it has to fill in the Contact that is logged in.
the notification_to has to be filled to so when there is no business added it has to take the latitude and the longitude and add the right area.
I have written the code but it won't work.
First I added it in the controller. But after looking around on google and this site I found I had to add it to the model.
But it still won't work.
What do I do wrong?
I get an error
class Notification < ActiveRecord::Base
belongs_to :people, :polymorphic => true
belongs_to :notification_to, :polymorphic => true
belongs_to :App_of_area
belongs_to :kind
before_validation :if_empty_add_the_other
#validates :photo, presence: true
validates :message, :status, :people_id, :people_type, :notification_to_id, :notification_to_type, :App_of_area_id, :kinds_id, presence: true
def if_empty_add_the_other
unless self.people_type.present?
self.people = current_contact
end
unless self.notification_to_id.present?
if self.longitude && self.latitude
#noa = NotificationArea.where( "latitude_A <= #{self.latitude} AND latitude_B >= #{self.latitude} AND longitude_A <= #{self.longitude} AND longitude_B >= #{self.longitude}")
self.notification_to = #noa.first
end
end
end
end
end
First thing first you should DRY up your code:
belongs_to :business
belongs_to :app_of_area
has_many :people, source_type: "Notification"
has_many :api_keys
before_validation :if_empty_add_the_other
has_secure_password
validates :name, :email, :rights, :password_confirmation, :app_of_area_id, :business_id, presence: true
validates_format_of :email, :with => /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
validates :email, uniqueness: true
validates :password, confirmation: true, length: { minimum: 6 }
:as is used for polymorphic associations, I believe you want to use :source_type as shown above. This would allow you to perform self.people << current_contact, but I address this more below.
Why was :App_of_area capitalized? Same with :App_of_area_id?
Your if_empty_add_the_other validation method has a lot wrong with it.
Use unless rather than if not.
Can the two if statements testing latitude and longitude be combined to if self.longitude && self.latitude?
You have to ask yourself, how is current_contact being passed to this function? Also, you're trying to set self.people equal to this phantom current_contact; self.people would contain multiple records, an array if you will, so setting an array equal to an object won't work, hence the self.people << current_contact above.

When should I validate the presence of an ActiveRecord association object vs its id?

Suppose I have an ActiveRecord association like:
class City < ActiveRecord::Base
belongs_to :state
end
A city without a state should be invalid. It seems that both of these are possible validations:
validates :state, presence: true
# OR
validates :state_id, presence: true
I would guess that they are identical, since:
belongs_to creates methods state and state=
state= sets the state_id
However, I've just fixed a failing spec by changing it to check for the id instead of the object.
Are these two ways of validating both acceptable? If so, when would you use one or the other?
validates :state will use the relationship from city to state (the belongs_to) along with the foreign key whereas validates :state_id alone will just use the column state_id and see if it has any value at all.
My preferred method is to validate state (the relationship) as this requires both the key and the relationship to be present.
Validating state_id will work, in that it will make sure that a state id exists, however it won't check for the validity of the code, i.e. that a state actually 'exists' for any given state key in City.
Basically if the foreign keys (for state_id) used in City all exist as actual records in State, the effect is the same. The difference would show if you had an invalid state code in state.
What if you did something like
s = State.new
c = City.new
c.state = s
c.valid?
I haven't tried this but I'm guessing that, if you're checking for the presence of c.state_id, it will be missing, even though c does have a state (because the ID hasn't been generated yet, because the state hasn't been saved yet).
That is to say, if what you care about is the presence of the state, you should validate the presence of the state.
Personally, I prefer to allow the model to be more robust and accept either or. So in your particular situation, City could accept either a State object or a state_id, but is required to send one of them.
class City < ActiveRecord::Base
attr_accessible :state, :state_id
validates :state, presence: true, if: proc{|c| c.state_id.blank? }
validates :state_id, presence: true, if: proc{|c| c.state.blank? }
belongs_to :state
end
Edit: Removed the double negative in the validate statement. Originally had unless: proc{|c| !c.state_id.blank? }
According to Rails 4 Way by Obie Fernandez:
When you're trying to ensure that an association is present, pass its
foreign key attribute, not the association variable itself
validates :region_id, :presence => true
validate :region_exists
def region_exists
errors.add(:region_id, "does not exist") unless Region.exists?(region_id)
end
The book does not explain why you should use this as opposed to
validates :region, :presence => true
But I know that these guys know their stuff.

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