Rails validates uniqueness cross models - ruby-on-rails

I have two models: event.rb and bag.rb
An event has an attribute called slug and a bag has an attribute called bag_code.
I generate a view based on the bag_code attribute. E.g. if the bag-code is "4711" I load records based on that code.
Now for some occasions, a user can define a slug URL in an event and in this case it should overwrite the bag-code.
What I don't want is that a user can choose a slug with a value which is already a bag_code (in this case it should be forbidden to choose the slug "4711") as this would cause troubles in my view, so it has to be unique in two models attributes.
I tried to solve this via scope
validates_uniqueness_of :slug, scope: [:bag_code]
but that would only work within the same model.
The association between my models is:
event.rb
has_many :bags

A custom validation method would work for this case. Here is how you would set it up:
event.rb
validates :slug, uniqueness: true
validate :slug_is_unique_from_bag_codes
private
def slug_is_unique_from_bag_codes
if Bag.find_by bag_code: slug
errors.add :base, "The slug is already being used as a bag code"
end
end

Related

Rails 4 cascade save association with validation on belongs_to

I'm having troubles autosaving objects which are created this way. Consider having following models:
class SearchTerm < ActiveRecord::Base
has_many :search_term_occurrences, dependent: :destroy
end
class SearchTermOccurrence < ActiveRecord::Base
belongs_to :search_term
validates :search_term, presence: true # Problematic validation
validates_associated :search_term # See Gotchas in answer
end
So when I try to do the following:
term = SearchTerm.new term: 'something'
term.search_term_occurrences << [SearchTermOccurrence.new, SearchTermOccurrence.new]
term.save!
I get the following:
ActiveRecord::RecordInvalid: Validation failed: Search term occurrences is invalid, Search term occurrences is invalid
But when I omit the validation on belongs_to search_term. Everything is saved properly.
My question is: How to save parent object and its association (newly created) whilst having validations on child objects without saving associated objects one by one and then saving parent object within transaction? I want Rails to handle the transactional logic.
So after some playing with Rails console I've found an answer. The problem was the order of saving.
I chose different approach of saving: Instead of going from top to bottom (SearchTerm has_many -> SearchTermOccurrences) I tried to save it from the bottom up (SearchTermOccurrence belongs_to -> SearchTerm). So instead of doing this:
term = SearchTerm.new term: 'something'
term.search_term_occurrences << [SearchTermOccurrence.new, SearchTermOccurrence.new]
term.save!
I did this:
occurrence = SearchTermOccurrence.new
occurence.search_term = SearchTerm.new(term: 'search query')
occurence.save!
This suddenly worked. So I'm guessing that Rails can only validate parent records in belongs_to association while newly created but cannot validate has_many child records.
EDIT 1
Gotchas:
If you are choosing this cascade save approach, make sure you add validates_associated for belongs_to record otherwise you might end up with inconsistent database, since validates :search_term, presence: true can pass even though SearchTerm itself is invalid. I've added validates_associated to SearchTermOccurrence.
you should save the term instance first before adding SearchTermOccurrence objects into it.
term = SearchTerm.new term: 'something'
term.save!
term.search_term_occurrences << [SearchTermOccurrence.new, SearchTermOccurrence.new]
because SearchTermOccurrence have validation of it's parent SearchTerm present true so SearchTerm.new will not create any ID of SearchTerm into database until it is saved.
try above code

Rails 4 - Remove attribute name from error message for associated model presence

I have two models user_item and user_item_images.
user_item.rb
has_many :user_item_images, dependent: :destroy
validates :user_item_images, presence: { message: "You must include a picture" }
user_item_images.rb
belongs_to :user_item
I have a nested form with only one user_item_image field which is :picture. When I submit an empty form I get this message
User item images You must include a picture
How do I make it so that the message instead says
You must include a picture
I don't know how to edit the en.yml file because the error is on the presence of another model and not an attribute of a model.
I looked here but the answer is too broad and I think I need a custom validation.
Create a custom validation instead:
has_many :user_item_images, dependent: :destroy
validate :has_a_picture
private
def has_a_picture
errors.add(:base, 'You must include a picture') if user_item_images.none?
end
For that, in your user_item_images model you need:
validates :picture, presence: true
You may also want to look into whether you have data modeling issues to resolve. Regardless, your current validation only validates that the association exists. So, when you submit an user_item_images form with an empty picture field the validation that fails is the one in your user_item model. Rather than attempt to change the content of association presence validation error message, add a validation for the picture column in the user_item_images. That way if picture is empty, it will raise 'You must include a picture'.
I'll also comment that having the presence validation :user_item_images on the user_item model shouldn't be necessary and likely indicates a larger data modelling issue that you need to resolve.
There's no need to write extra code. You should use Rails' built in I18n to customise ActiveRecord error messages.
Inside config/locales/en.yml
en:
activerecord:
errors:
models:
user_item:
attributes:
user_item_images:
presence: "You must include a picture"

ActiveRecord validates inclusion in list - list isn't updated after new associated model created

I have a Company model and an Employer model. Employer belongs_to :company and Company has_many :employers. Within my Employer model I have the following validation:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
I'm running into a problem where the above validation fails. Here is an example setup in a controller action that will cause the validation to fail:
company = Company.new(company_params)
# company_params contains nested attributes for employers
company.employers.each do |employer|
employer.password = SecureRandom.hex
end
company.employers.first.role = 'Admin' if client.employers.count == 1
company.save!
admin = company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update(some_attr: 'some_val')
On the last line in the example code snippet, admin.update will fail because the validation is checking to see if company_id is included in the list, which it is not, since the list was generated before company was saved.
Obviously there are ways around this such as grabbing the value of company.id and then using it to define admin later, but that seems like a roundabout solution. What I'd like to know is if there is a better way to solve this problem.
Update
Apparently the possible workaround I suggested doesn't even work.
new_company = Company.find(company.id)
admin = new_company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update
# Fails validation as before
I'm not sure I understand your question completely, but there is an issue in this part of the code:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
The validation is configured on the class-level, so it won't work well with updates on that model (won't be re-evaluated on subsequent validations).
The docs state that you can use a block for inclusion in, so you could try to do that as well:
validates :company_id, inclusion: {in: ->() { Company.pluck(:id).prepend(nil) }}
Some people would recommend that you not even do this validation, but instead, have a database constraint on that column.
I believe you are misusing the inclusion validator here. If you want to validate that an associated model exists, instead of its id column having a value, you can do this in two ways. In ActivRecord, you can use a presence validator.
validates :company, presence: true
You should also use a foreign key constraint on the database level. This prevents a model from being saved if there is no corresponding record in the associated table.
add_foreign_key :employers, :companies
If it gets past ActiveRecord, the database will throw an error if there is no company record with the given company_id.

Rails conditional validation in model

I have a Rails 3.2.18 app where I'm trying to do some conditional validation on a model.
In the call model there are two fields :location_id (which is an association to a list of pre-defined locations) and :location_other (which is a text field where someone could type in a string or in this case an address).
What I want to be able to do is use validations when creating a call to where either the :location_id or :location_other is validated to be present.
I've read through the Rails validations guide and am a little confused. Was hoping someone could shed some light on how to do this easily with a conditional.
I believe this is what you're looking for:
class Call < ActiveRecord::Base
validate :location_id_or_other
def location_id_or_other
if location_id.blank? && location_other.blank?
errors.add(:location_other, 'needs to be present if location_id is not present')
end
end
end
location_id_or_other is a custom validation method that checks if location_id and location_other are blank. If they both are, then it adds a validation error. If the presence of location_id and location_other is an exclusive or, i.e. only one of the two can be present, not either, and not both, then you can make the following change to the if block in the method.
if location_id.blank? == location_other.blank?
errors.add(:location_other, "must be present if location_id isn't, but can't be present if location_id is")
end
Alternate Solution
class Call < ActiveRecord::Base
validates :location_id, presence: true, unless: :location_other
validates :location_other, presence: true, unless: :location_id
end
This solution (only) works if the presence of location_id and location_other is an exclusive or.
Check out the Rails Validation Guide for more information.

Can validate_uniqueness_of work with custom scopes?

I'm working on an RoR project and I'd like to have a uniqueness validation on one of my models that checks against a custom scope:
class Keyword < ActiveRecord::Base
belongs_to :keyword_list
scope :active, -> { where("expiration > ?", DateTime.now) }
validates :name, uniqueness: { scope: [:active, :keyword_list_id] }
end
Only, this doesn't work. It checks the database for an active column, which doesn't exist and throws this error:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column keywords.active does not exist
So, my question is there any way to make this work, or do I have to write a custom validator? And if so, are there any tips on what it should look like to hitting the database too much?
No, you will have to write a custom validation.
Try this.
# In app/models/keyword.rb
validate :freshness
private
def freshness
if Keyword.active.find_by(name: self.name, keyword_list_id: self.keyword_list_id)
errors.add(:base, "Name is not fresh enough.") # Fails the validation with error message
end
end
Here's another interesting point, you cannot rely on validates_uniqueness_of, or any other uniqueness validator in rails, because validations are not run atomically, meaning that if two identical records are inserted at the same time, and there is no SQL constraint validating uniqueness, the ruby validations will pass and both records will be inserted.
What I'm trying to say here is that if your validations are mission-critical, use a SQL constraint.

Resources