Rails - force field uppercase and validate uniquely - ruby-on-rails

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.

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

Field becomes blank when before_validation is used in Rails Admin

I am using the Rails Admin gem. When I add a new activity type and create it again with the same name, it validates that name is already taken. But whenever I try to edit one it will give you an error: "name can't be blank"
For example, I created Swimming, and I tried to add a new activity type which is swimming/SWIMMING etc. To avoid this I used the before_validation callback, to make the first letter a capital, then check the uniqueness of name.
Yes, it's working but whenever I try to edit the name field it will become blank after I submit it.
NOTE: I also tried to use validates :name, presence: true, :uniqueness => {:case_sensitive => true} only without the before_validation but it didn't work.
Activity Type
class ActivityType < ApplicationRecord
before_destroy :ensure_has_no_activity_type
before_validation :capitalize_first_letter_name
has_many :activities
validates :name, presence: true,:uniqueness => {:case_sensitive => true}, length: { maximum: 20 },format: Utilities::RegexValidations.alphanumeric_underscore
validates :description, presence: false
private
def ensure_has_no_activity_type
if activities.present?
errors.add(:base, 'Cannot delete activity type that has activity')
throw(:abort)
end
end
def capitalize_first_letter_name
# Capitalize the first letter and the rest will be small letter
self.name = self.name.capitalize!
end
end
Question: Why whenever I tried to edit and try to submit it, does the name field become blank? What is the reason for this?
The problem arises from capitalize_first_letter_name. "".capitalize! will return nil. If you change it to "".capitalize that will return blank string as expected.
Moreover, capitalize! will return nil if no changes were made. See https://ruby-doc.org/core-2.2.0/String.html#method-i-capitalize-21.

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.

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

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.

Resources