how to validate a single attribute from a nested object - ruby-on-rails

I am new to Rails and Ruby. On my view, I have 2 radio buttons that ask if the person is a resident of the US. If they are, a state select is shown. If they aren't, a country select is shown.
I am trying to validate that a state was selected, if the person is a resident of the US.
How can I create a validation and access the state out of the addresses_attributes?
Here is my model:
class Person < ActiveRecord::Base
has_many :addresses, :as => :addressable
has_one :user
accepts_nested_attributes_for :user, :allow_destroy => true
accepts_nested_attributes_for :addresses
attr_accessor :resident
attr_accessible :campaign_id,
:first_name,
:last_name,
:user_attributes,
:addresses_attributes,
:resident
validates :first_name, :presence => true
validates :last_name, :presence => true
validates_presence_of :resident, :message => "must be selected"
end
These are the relevant parameters being sent:
"resident"=>"true",
"addresses_attributes"=>{"0"=>{"country_code"=>"",
"state"=>""}}

You need custom validation method.
validate :check_state_presence
def check_state_presence
if self.resident && !self.addresses.state.present?
self.errors[:base] << "You need to Select State if you are a US resident."
end
end

You can sort it out using validates_inclusion_of instead.
Ruby API says:
If you want to validate the presence of a boolean field (where the real values are true and >false), you will want to use validates_inclusion_of :field_name, :in => [true, false].
This is due to the way Object#blank? handles boolean values: false.blank? # => true.

+1 to #VelLes for the help in pointing me in the right direction. I am answering my own question because I had to change #VelLes example a bit to get it to work and I want other people to see the full solution.
Since I am using attr_accessor as a virtual attribute, when the true/false value comes in from the radio button, it gets stored as a string. Therefore if self.resident = "false", it will get evaluated to true.
You can do self.resident == 'false' or convert to a boolean and add a new self.resident? method. I chose the latter.
The boolean conversion came from this blog post, add to a file in config/initializers
class String
def to_bool
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
end
end
My final code is:
validate :check_state_presence
def resident?
resident.to_bool
end
def check_state_presence
if self.resident? && !self.addresses[0].state.present?
#the inline version of the error message
self.addresses[0].errors.add(:state, "must be selected")
end
end
Please let me know if there is a better 'rails' way to do this!

Related

Making field validate only if user specified a certain value, otherwise optional

There is a form that a user submits which then gets validated by the model. I only want the field "Province / State" to validate if the "Country" is either "CA" (Canada) or "US" (USA)
The form is set up a little differently because we are doing a multiple step from process.
Here is the controller.
def update
case step
when :step1
wizard_params = profile_params()
wizard_params[:wizard] = 'step1'
#profile = current_user.profile
#profile.update(wizard_params)
render_wizard #profile
end
private
def profile_params
# There are more params although I stripped them for the simplicity of this example
params.require(:profile).permit(:state_id, :country)
end
Profile.rb
belongs_to :state, :class_name => "ProvinceState", :foreign_key => :state_id, optional: true
I hard coded optional: true but I only want optional:true if the user selected CA/US OR the field saved is CA/US.
I took a look at lambda and it could be something I need.
For Example:
belongs_to :state, :class_name => "ProvinceState", :foreign_key => :state_id, optional: lambda | obj | self.country == CA || self.country == US ? true : false
Unfortunately, you cannot (currently) provide a lambda to optional - see the source code:
required = !reflection.options[:optional]
If required, Rails just adds a presence validation like this:
model.validates_presence_of reflection.name, message: :required
Therefore as a workaround, you could do this in two parts: First specify the association as optional; then explicitly make it required for your condition:
belongs_to :state, :class_name => "ProvinceState", :foreign_key => :state_id, optional: true
validates :state_id, presence: true, if: ->{ %w[CA US].include?(country) }
If the logic gets significantly more complicated, you may wish to move this into a separate method/class instead of an inline lambda. See: Performing Custom Validations
You can make validation with lambda condition like this:
validates :state, presence: true, if: -> { %w[US CA].include?(country) }

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.

before_validation to prevent a duplicate category name

I have a categories model. I want to ensure that a user doesn't add a duplicate category name to his/her list of categories.
Here's my categories model:
class Category < ActiveRecord::Base
belongs_to :user
validates :name, presence: true
validates :user_id, presence: true
before_validation :validate
private
def validate
errors.add(:name, "is already taken") if Category.where("name = '?' AND user_id = ?", self.name, self.user_id).any?
end
end
Here is my RSpec test:
it "is invalid with duplicate name for same user" do
existing_category = Category.first
new_category = Category.new(:name => existing_category.name, :user_id => existing_category.user_id)
expect(new_category).to have(1).errors_on(:name)
end
Should I use before_save or before_validate? Also, I'm unsure how to write this. I guess if a duplicate is detected, I want to add an error for :name. Above is my attempt but doesn't seem to make it pass, is there anything obviously wrong? Also, is this good practise for adding custom validation?
Here is a much simpler way to achieve your goal - you can use scope option of validates_uniqueness_of validator:
validates_uniqueness_of :name, scope: :user_id
Your spec fails because it has an error. It expect new_category has error, but it doesn't run validations on this object. To do that, you just need to add:
new_category.valid?
before expect#... line.

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

Destroy on blank nested attribute

I would like to destroy a nested model if its attributes are blanked out in the form for the parent model - however, it appears that the ActiveRecord::Callbacks are not called if the model is blank.
class Artist < ActiveRecord::Base
using_access_control
attr_accessible :bio, :name, :tour_dates_attributes
has_many :tour_dates, :dependent => :destroy
accepts_nested_attributes_for :tour_dates, :reject_if => lambda { |a| a[:when].blank? || a[:where].blank? }, :allow_destroy => true
validates :bio, :name :presence => true
def to_param
name
end
end
and
class TourDate < ActiveRecord::Base
validates :address, :when, :where, :artist_id, :presence => true
attr_accessible :address, :artist_id, :when, :where
belongs_to :artist
before_save :destroy_if_blank
private
def destroy_if_blank
logger.info "destroy_if_blank called"
end
end
I have a form for Artist which uses fields_for to show the fields for the artist's associated tour dates, which works for editing and adding new tour dates, but if I merely blank out a tour date (to delete it), destroy_if_blank is never called. Presumably the Artist controller's #artist.update_attributes(params[:artist]) line doesn't consider a blank entity worth updating.
Am I missing something? Is there a way around this?
I would keep the :reject_if block but insert :_destroy => 1 into the attributes hash if your conditions are met. (This is useful in the cases where it's not convenient to add _destroy to the form code.)
You have to do an extra check to see if the record exists in order to return the right value but the following seems to work in all cases for me.
accepts_nested_attributes_for :tour_dates, :reject_if => :reject_tour, :allow_destroy => true
def reject_tour(attributes)
exists = attributes['id'].present?
empty = attributes.slice(:when, :where).values.all?(&:blank?)
attributes.merge!({:_destroy => 1}) if exists and empty # destroy empty tour
return (!exists and empty) # reject empty attributes
end
You could apply when all attributes are blank by just changing the empty calculation to:
empty = attributes.except(:id).values.all?(&:blank?)
I managed to do something like this today. Like #shuriu says, your best option is to remove the reject_if option and handle destruction yourself. mark_for_destruction comes in handy :
class Artist < ActiveRecord::Base
accepts_nested_attributes_for :tour_dates
before_validation :mark_tour_dates_for_destruction
def mark_tour_dates_for_destruction
tour_dates.each do |tour_date|
if tour_date.when.blank? || tour_date.where.blank?
tour_date.mark_for_destruction
end
end
end
end
You have code that says the record should be ignored if the 'where' or the 'when' is blank, on the accepts_nested _attributes line, remove the reject_if and your destroy_if blank will likely be called.
Typically to destroy, you would set a _destroy attribute on the nested record, check out the docs http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Also, just used cocoon for some of this today, and thought it was awesome, https://github.com/nathanvda/cocoon
Similar to Steve Kenworthy's answer, no local variables.
accepts_nested_attributes_for :tour_dates, :reject_if => :reject_tour, :allow_destroy => true
def reject_tour(attributes)
if attributes[:when].blank? || attributes[:where].blank?
if attributes[:id].present?
attributes.merge!({:_destroy => 1}) && false
else
true
end
end
end
With your current code it's not possible, because of the reject_if option passed to accepts_nested_attributes_for.
As Christ Mohr said, the easiest way is to set the _destroy attribute for the nested model when updating the parent, and the nested model will be destroyed. Refer to the docs for more info on this, or this railscast.
Or you can use a gem like cocoon, or awesome_nested_fields.
To do specifically what you want, you should remove the reject_if option, and handle the logic in a callback inside the parent object. It should check for blank values in the tour_dates_attributes and destroy the nested model. But tread carefully...

Resources