rails ActiveRecord validation on specific controller and action - ruby-on-rails

is it possible to run ActiveRecord validates on given controller and action.
For example I have user_controller and signup_controller
I need to run password required validation only on signup_controller#create action

You can run validations using an if conditional:
validates :email, presence: true, if: :validate_email?
Now you need to define this instance method (in your model):
def validate_email?
validate_email == 'true' || validate_email == true
end
This validate_email attribute could be a virtual attribute in your model:
attr_accessor :validate_email
And now, you can perform email validation depending on that virtual attribute. For example, in signup_controller#create you can do something like:
def create
...
#user.validate_email = true
#user.save
...
end

use validates :password, :if => :password_changed? in user.rb
if form in users_controller does not submit password field then you should be ok.

Just a tip for implementing #markets' answer
We can use
with_options if: :validate_email? do |z|
z.validates :email, presence: true
z.validates :name, presence: true
end
for multiple validations on our specific action.
Also, we use session to pass a variable which indicate params from this action will need some validations
Controller:
before_action :no_validate, only: [:first_action, :second_action, ..]
before_action :action_based_validation, only: [:first_action, :second_action, ..]
def first_action; end
def second_action; end
def update
..
#instance.validate = session[:validate]
..
if #instance.update(instance_params)
..
end
end
private
def no_validate
session[:validate] = nil
end
def action_based_validation
# action_name in first_action will = "first_action"
session[:validate] = action_name
end
Model
attr_accessor :validate
with_options if: "validate == 'first_action'" do |z|
z.validates :email, presence: true
..more validations..
end
with_options if: "validate == 'second_action'" do |z|
z.validates :name, presence: true
..more validations..
end
more details:
http://guides.rubyonrails.org/active_record_validations.html#conditional-validation

Related

Rails 6 validate model only for specific controller

I've got User model with validation:
validates :experience_level, inclusion: { in: EXPERIENCE_LEVEL, allow_blank: true }
But one of the part of full registration is to update User's experience level. User can do this by inside of below controller:
module Users
class ExperienceLevelsController < SignupBaseController
def edit
authorize current_user
end
def update
authorize current_user
if current_user.update(user_experience_level_params)
redirect_to new_appropriateness_test_step_one_path,
else
render :edit
end
end
end
And for that endpoint I want to use
validates :experience_level, presence: true, inclusion: { in: EXPERIENCE_LEVEL }
I know I could use on: :update but in such case User will not be able to update e.g. password if it doesn't go through the experience update form first.
If you want to make the model state aware you can do it by explicitly passing information into the model:
class User < ApplicationRecord
attr_accessor :stage
validates :experience_level,
inclusion: { in: EXPERIENCE_LEVEL }
validates :experience_level, presence: true, if: :requires_experience_level?
def requires_experience_level?
stage == :add_experience_level
end
end
module Users
class ExperienceLevelsController < SignupBaseController
def edit
authorize current_user
end
def update
authorize current_user
if current_user.update(user_experience_level_params.merge(stage: :add_experience_level))
redirect_to new_appropriateness_test_step_one_path,
else
render :edit
end
end
end
end
There is also ActiveSupport::CurrentAttributes:
Abstract super class that provides a thread-isolated attributes
singleton, which resets automatically before and after each request.
This allows you to keep all the per-request attributes easily
available to the whole system.
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :stage
end
def update
authorize current_user
Current.stage = :add_experience_level
end
class User < ApplicationRecord
attribute_accessor :stage
validates :experience_level,
inclusion: { in: EXPERIENCE_LEVEL }
validates :experience_level, presence: true, if: :requires_experience_level?
def requires_experience_level?
Current.stage == :add_experience_level
end
end
Its really up to you if you want use it though as it can be considered harmful. If it quacks like a global...

How to validate Rails model based on a parameter?

I have User model, and need to validate phone number attribute based on the controller param.
class User < ActiveRecord::Base
validates_presence_of :phone_number
end
This validation should validate phone_number in the Create action.
Let's say the param I should check is
params[:phone_number]
you can use before_save validation, in User model you can write
before_save :validate_phone_number
private
def validate_phome_number
self.phone_number = /some regex/
end
In self.phone_number you will get controller params by default
validate :custom_validation, :on => :create
private
def custom_validation
//whatever you want to check
end
I have tried many ways to complete this task,
I used Inheritance - Created a sub class from the User class
Call a method in the model from the controller to set the attribute and bind that attribute with the validation
Use the context option
I guess the context option is the most reliable solution for this issue i faced. So here when i set the context as :interface the model validation will trigger only based on that value
Model - User.rb
class User < ActiveRecord::Base
validates_presence_of :phone_number, on: :interface
end
Controller - users_controller.rb
#user = User.new(user_params)
#save_result = false
if params[:invitation_token] == nil
save_result = #user.save(context: :interface)
else
save_result = #user.save
end
If you use multiple options in ON:
validates :terms_and_conditions, acceptance: {accept: true}, on: [:create, :interface], unless: :child
validates :privacy_policy, acceptance: {accept: true}, on: [:create, :interface], unless: :child

Rails validations depending on the value that may be not present

I've got a service object which has several validations that validate two params. Everything works fine until these params are "". In this case, even though I validate their presence, later validations raise errors. How can I make my code validate the presence first and then, only if the values are present, continue the validations?
class SubscriptionPause
include ActiveModel::Model
extend ActiveModel::Naming
attr_accessor :paused_from, :paused_till, :params, :id
validates :paused_from, presence: true, allow_nil: true
validates :paused_till, presence: true, allow_nil: true
validate :paused_from_cant_be_later_than_subscription_ends
validate :paused_from_cant_be_in_the_past
validate :paused_till_is_later_than_paused_from
def initialize(params)
#params = params
#paused_form = params[:paused_from]
#paused_till = params[:paused_till]
end
def create
if valid?
...
else
...
end
end
private
def subscription
#subscription || Subscription.find(params[:id])
end
def paused_from_cant_be_in_the_past
if !paused_from.empty? && paused_from.to_date < Date.today
errors.add(:paused_from, I18n.t("..."))
end
end
def paused_till_is_later_than_paused_from
if paused_from > paused_till
errors.add :paused_from, I18n.t("...")
end
end
def paused_from_cant_be_later_than_subscription_ends
if !paused_from.empty? && subscription.expire_date < paused_from
errors.add :paused_from, I18n.t("...")
end
end
end
Based on your comment above, it sounds like you never want the from or till to be nil so remove allow_nil: true. Then just add a conditional to the other validations as suggested by Rahul
validates :paused_from, presence: true
validates :paused_till, presence: true
validate :paused_from_cant_be_later_than_subscription_ends, if: :params_present?
validate :paused_from_cant_be_in_the_past, if: :params_present?
validate :paused_till_is_later_than_paused_from, if: :params_present?
def params_present?
paused_from.present? && paused_till.present?
end
P.S. don't use and over && unless you know why (suggested by Rahul). && is better in nearly all cases. Difference between "and" and && in Ruby?
You could do something like this:
validate :paused_from_cant_be_later_than_subscription_ends, :if => :params_present?
validate :paused_from_cant_be_in_the_past, :if => :params_present?
validate :paused_till_is_later_than_paused_from, :if => :params_present?
def params_present?
return params[paused_from].present? and params[paused_till].present?
end

confusing validation error in rails model

Below is my model and controller from which i have filtered unneccessary lines.
class User < ActiveRecord::Base
validates :password, presence: true, on: :create
validates :password_confirmation, presence: true, if: "password.present?"
validates :password, confirmation: true,
length: { in: 6..20}, if: "password.present?"
end
and controller-
class UsersController < ApplicationController
def update
#user = User.find(params[:id])
if params[:password].present?
final_params = get_params
else
final_params = get_params.delete_if { |k,v|
k == "password" or k == "password_confirmation"
}
end
if #user.update(final_params)
redirect_to #user
else
render 'edit'
end
end
private
def get_params
params.require(:user).permit(:first_name, :last_name, :email, :date_of_birth,
:password,:password_confirmation, :mobile, :country, :state, :city, :pincode)
end
end
the problem is when updating a data, it shows a validation error i.e password confirmation can not be blank. even if I enter something to that field and submit. and to find error i tried replacing "password.present?" from password confirmation validation with "password.exists?" and it showed exception that exists is not a valid method for "123456 : string" . 123456 is the current password in DB. why is it checking password against db ? and please help me to solve this.
if params[:password].present?
final_params = get_params
else
final_params = get_params.delete_if { |k,v| k == "password" or k == "password_confirmation"}
end
Your problem is the first line here... your params are params[:user][:password] not params[:password] (you can see that in your get_params method)
So always your code is going to run the section that removes the password/confirmation
Also:
validates :password_confirmation, presence: true, if: "password.present?"
using a string of ruby in the validation is not generally considered good practice. How about adding a method like so:
validates :password_confirmation, presence: true, if: :confirmation_needed?
def confirmation_needed?
password.present?
end
Finally, you also need to not check the length of password_confirmation if it hasn't actually been entered:
validates :password, confirmation: true, length: { in: 6..20},
allow_blank: true, if: :confirmation_needed?
It's never a good idea to chunk down the incoming parameters in the controller.
Rather, putting proper validations in the model is a good idea !
hence cleaned up your controller.
Check below code:
class UsersController < ApplicationController
def update
#user = User.find(params[:id])
redirect_to #user and return if #user.update_attributes(get_params)
# render will not be executed if the user is redirected & returned
render :edit
end
private
def get_params
params.require(:user).permit(:first_name, :last_name, :email, :date_of_birth,
:password, :password_confirmation, :mobile, :country :state, :city, :pincode)
end
end
modified model:
class User < ActiveRecord::Base
validates :password, presence: true, on: :create
# above validation will be effective only for during new record creation.
# below 2 validations will be cheked only if password is present in the params list.
validates :password, confirmation: true,
length: { in: 6..20 }, if: validate_password?
validates :password_confirmation, presence: true, if: validate_password?
private
def validate_password?
password.present?
end
end
if still this does not help, then try to debug the self object in the validate_password? method. use raise self.inspect in the validation method to verify the incoming parameters.
That way you can track where you are going wrong.

How to use with_options for conditional validation

How can I use with_options for conditional validation ?
My code is
with_options if: (AppUser::User.creator=="is_admin") do |admin|
admin.validates :first_name, :presence => true
admin.validates :last_name, :presence => true
end
I have already set creator method in application controller.
before_action :set_global_user
def set_global_user
if current_admin
AppUser::User.creator= "is_admin"
elsif current_user
AppUser::User.creator= "is_user"
else
AppUser::User.creator=nil
end
end
but I am getting
undefined method `validate' for false:FalseClass
what is wrong with this code.
because
(AppUser::User.creator == "is_admin")`
does not return an object but it is a boolean.
Try this (inside your model):
class User < ActiveRecord::Base
with_options if: (AppUser::User.creator == "is_admin") do
validates :first_name, presence: true
validates :last_name, presence: true
end
end
P.S: I recommend the use of the devise gem to manage user types like this:
devise and multiple “user” models.

Resources