Ruby on Rails validating uniqueness on other column presence - ruby-on-rails

How to validate uniqueness in model based on if other column has value or not, not on actual content? Scope seems to compare the content of the second column. For example I have in my User model columns email and project_id (both string) among others. I want to validate email to be unique if project_id is null or has any value. Using scope allows creating objects {email: 'a#a.a', project_id: nil}, {email: 'a#a.a', project_id: '1'}, {email: 'a#a.a', project_id: '2'} and so on. I want to limit the email uniqueness so that those first two objects would be possible (project_id is nil or 1 with same email) and last object would throw error 'email has already been taken' because there's already user with same email when project_id has value. Is there proper rails way to achieve that or do I need to write some custom validation?
Of course I have better email validation also and wouldn't accept 'a#a.a', that's just an example :)

The scope option in uniqueness validation is not the same as a Rails scope. It's more related to SQL and is generally restricted to just an attribute names.
So, since you can't use custom scope in uniqueness validation, you have to write custom validation for this purpose.
validate :validate_email_on_project_id_existence
def validate_email_on_project_id_existence
if User.where(email: self.email).where(self.project_id.nil? ? 'project_id IS NULL' : 'project_id IS NOT NULL').first
errors.add(:email, "some error")
end
end

You can create your custom validation and call it with validates_with, in your case it'd be something like:
class ExampleEmailUniqueness < ActiveModel::Validator
def validate(record)
error_message = "has already been taken"
if record.project_id.nil? && Example.where(project_id: nil, email: record.email).exists?
record.errors.add :email, error_message
elsif Example.where(email: record.email).where.not(project_id: nil).exists?
record.errors.add :email, error_message
end
end
end
class Example < ApplicationRecord
validates_with ExampleEmailUniqueness
end
That validation against your scenario would yield the results:
Example.new(email: 'a#a.a').save # Saved
Example.new(email: 'a#a.a', project_id: '1').save # Saved
Example.new(email: 'a#a.a', project_id: '2').save # Email has already been taken
Example.new(email: 'a#a.a').save # Email has already been taken
Note that saving a model with a nil project_id AFTER saving one that isn't nil will still pass validation and get saved to the database, which I'm not sure if it's the intended behavior.

Related

Custom form Validation Rails

I am wondering if i have set up my method incorrectly as my validation attempt is being ignored halfway through the method. (i have provided a basic example, i want to get this working before adding more)
I have two variations of a form all belonging to the same object. The forms are differentiated by a column animal_type as you can see in my method
class Animal < ActiveRecord::Base
before_save :animal_form_validation
private
def animal_form_validation
if self.animal_type == 'Dog'
ap('Im validating the dog Form')
if self.name.length <= 0
errors.add(:name, "Name cannot be blank")
end
elsif self.animal_type == 'Cat'
ap('Im validating the cat form')
if self.name.length <= 0
errors.add(:name, "Name cannot be blank")
end
end
end
end
whether i am submitting a cat or dog i get the correct message in the console (using awesome print), so the method is running and knows which form im submitting, but as for the next if statement it is being ignored.
So i have an error with my syntax? or am i calling the wrong validation check on the name field ?
Thanks
Use validation instead of a before_save callback:
validate :animal_form_validation
Also, you can add conditional validation if you're checking the same condition prior to validating. Example: validate :animal_form_validation, if: name.blank?
You are calling those validation in before_save, which is after the record has been validated and is assumed to be valid. You need to run it as a validation:
validate :animal_form_validation

find_or_create_by with attribute which presence: true in Rails

I have the following User model:
class User < ActiveRecord::Base
validates :email, presence:true
end
When I do:
#user = User.find_or_create_by(:email => params[:email])
I was doing #user.nil? In order to see if the user is there, but it is not acting as I thought:
puts #user.inspect
--> #<User id: nil, email: "", created_at: nil, updated_at: nil>
Why is a user gets created, even though not saved in the DB? How can I make sure that the #user is "nil"? #user.id.nil?
Thanks
The User record was not saved because it has a blank email (""), so the presence validation fails. You can check the error messages with #user.errors.full_messages.
With Active record calling .create (or one of its derivatives) will return a new record if the save operation fails (including validations). If you want an Error to be raised when the User record is invalid you can use .create!, with a bang(!). I would suggest this newer syntax which is compatible with Rails 4:
# Raises an Error if no matching record is found and the new record fails to save.
#user = User.where(:email => params[:email]).first_or_create!
# Does not raise an error if new record fails to save. The returned object will be a new record with post-validation error states.
#user = User.where(:email => params[:email]).first_or_create
Which method you choose depends on how you want your application to behave.
You can ask if it is a new record via:
#user.new_record?

Adding custom fields to a class at runtime, in Ruby with mongoid

In a project came across a requirement wherein a logged in user should be asked specific data based on his company. This specific data would be company specific, and could be mandatory or unique. This is the approach I took.
1. Defined a model to with three fields: Label (string), Mandatory(boolean), Unique(boolean).
2. The admin of the company could then enter the required fields for. e.g: Label => "Employee number", Mandatory => true, Unique => false using a simple form.
3. This data should be asked at the time of creating another record of model Redeemed Coupon for logged in user.
4. So during initialize of the Redeemed Coupon model, reopening the class, and checking the logged in user's company.
class RedeemedCoupon
def initialize(attrs = nil, options = nil)
super
if Merchant.current #this is set in the application controller as thread specific variable
coupon_custom_field = CouponCustomField.where(:merchant_id => Merchant.current).first
if coupon_custom_field and coupon_custom_field.custom_fields.size > 0
coupon_custom_field.custom_fields.each do |custom_field|
class_eval do
field custom_field.label.to_sym, :type => String
attr_accessible custom_field.label.to_sym
end
if custom_field.unique
class_eval do
index custom_field.label.to_sym
#validates_uniqueness_of custom_field.label.to_sym, :case_sensitive => false
end
end
if custom_field.mandatory
class_eval do
#validates_presence_of custom_field.label.to_sym
end
end
end
end
end
end
However the validations validates presence of and uniqueness does not work, with a failure message being given : callback not defined. this is thrown before save, when is_valid? is called object.
TO work around that
Put in custom validation
validate :custom_mandatory_unique
def custom_mandatory_unique
if Merchant.current
coupon_custom_field = CouponCustomField.where(:ira_merchant_id => Merchant.current).first
if coupon_custom_field and coupon_custom_field.custom_fields.size > 0
coupon_custom_field.custom_fields.each do |custom_field|
field_value = self.send(custom_field.label.to_sym)
self.errors.add(custom_field.label.to_sym, "cannot be blank") if !field_value.present? and custom_field.mandatory
if field_value.present? and custom_field.unique
if RedeemedCoupon.where(custom_field.label.to_sym => field_value, :merchant_id => Merchant.current).size > 0
self.errors.add(custom_field.label.to_sym, "already taken")
end
end
end
end
end
end
My questions:
1. Is this the best way of doing it.
2. Are there any gems already present (have searched, however couldnt get any)?
3. How can i use the validation helpers here instead of defining a seperate validate block?
I would define a model that stores the set of attribute mappings that correspond to a company, and an attribute model that holds its values and is associated with your coupon model. Then create a custom validation method in coupon that makes sure all of the require attributes are present based on the company id, and a method that builds them per the company association.

How to verify that the association is valid

I have a model called Profile which is belong_to User, so there is 'user_id' for the database to keep track of. In the local admin interface I made for this model I wanted to provide the flexibility of allowing admin to enter an username to a field in the editing screen, and then resolve that to user_id for saving in controller.
However the question is, how do I check against that the username have a valid return? I found that in ActiveRecord::Validation there is no method for validating the existence of the association. How will you handle a situation like this?
Update: What I want to do is to validate that the username field in the form is indeed a real user, then I could save that user_id back to the profile admin is editing. Here 'return' means the user object returned.
This problem is a good candidate for virtual attributes.
Instead of trying to resolve the username, let the profile model to the job for you.
class Profile
belongs_to :user
# ...
def username
user.try(:username)
end
def username=(value)
self.user = User.find_by_username(value)
end
end
Then in your form
<% form_for #profile do |f| %>
<%= f.text_field :username %>
<% end %>
When submitted, the value for the username field is automatically passed with all the other real activerecord attributes. ActiveRecord will look for the username= setter and will resolve the association.
If the association returns nil (no record exists with given username), then it will set current user_id to nil and validation will fail as expected.
You might want to customize the error code to make more meaningful.
EDIT: Added example.
validate :ensure_username_exists
def username=(value)
self.user_id = User.find_by_username(value) || 0
end
protected
def ensure_username_exists
if user_id == 0 # nil is allowed
errors.add(:user_id, "Username doesn't exists")
return false
end
end
This is a useful reference for Active Record Associations: http://guides.rubyonrails.org/association_basics.html
To check for the existence of the association, just check association.nil?
if #profile.user.nil?
... something ...
end
To check if the username has a valid return, well I'm not quite sure what you mean. Could you expand on that?

Ruby on Rails: errors.add_to_base vs. errors.add

I have read that errors.add_to_base should be used for errors associated with the object and not a specific attribute. I am having trouble conceptualizing what this means. Could someone provide an example of when I would want to use each?
For example, I have a Band model and each Band has a Genre. When I validate the presence of a genre, if the genre is missing should the error be added to the base?
The more examples the better
Thank you!
It's worth noting (since this shows up in the search engines, which is how I found it) that this is deprecated. The Rails 3 way of doing it is below but is no longer valid as of Rails 7 (see the comment from April 2022)
errors[:base] << "Error message"
the preferred way of doing it is
errors.add(:base, "Error message")
http://apidock.com/rails/ActiveRecord/Errors/add_to_base
http://apidock.com/rails/v3.2.8/ActiveModel/Errors/add
A missing genre would be a field error. A base error would be something like an exact duplicate of an existing record, where the problem wasn't tied to any specific field but rather to the record as a whole (or at lest to some combination of fields).
In this example, you can see field validation (team must be chosen). And you can see a class/base level validation. For example, you required at least one method of contact, a phone or an email:
class Registrant
include MongoMapper::Document
# Attributes ::::::::::::::::::::::::::::::::::::::::::::::::::::::
key :name, String, :required => true
key :email, String
key :phone, String
# Associations :::::::::::::::::::::::::::::::::::::::::::::::::::::
key :team_id, ObjectId
belongs_to :team
...
# Validations :::::::::::::::::::::::::::::::::::::::::::::::::::::
validate :validate_team_selection
validate :validate_contact_method
...
private
def validate_contact_method
# one or the other must be provided
if phone.empty? and email.empty?
errors.add_to_base("At least one form of contact must be entered: phone or email" )
end
end
def validate_team_selection
if registration_setup.require_team_at_signup
if team_id.nil?
errors.add(:team, "must be selected" )
end
end
end
end

Resources