Triggering only certain validations on a particular action with Rails and devise

427 Views Asked by At

I have and Administrator model with devise :

class Administrator < ApplicationRecord
     validate :max_num_admins, on: :create
     devise :invitable, :database_authenticatable,
         :recoverable, :rememberable, :validatable
     
       def max_num_admins
         self.errors.add(:base,
          I18n.t(
           'activerecord.errors.models.administrator.max_reached'
          )
         ) if Administrator.count > 3
        end
      end

Admins can be added through an admin dashboard by the "account owner", this has the effect of inviting the admin (devise invitable method invite!) :

def create
  resource = resource_class.new(resource_params)
  authorize_resource(resource)
  if resource.valid?
    Administrator.invite!(resource_params)
    redirect_to(
      [namespace, resource],
      notice: translate_with_resource('create.success')
    )
  else
    render :new, locals: {
      page: Administrate::Page::Form.new(dashboard, resource)
    }
  end
end

As you can see, I want to check if my record is valid, especially if there are no more than 3 admin records for that account. The problem is when I call valid? on resource(the administrator) I always get false as at this stage the only necessary param to invite an admin is a valid email but devise validations are triggered, and thus I get an error for not setting a password.

What would be a clean way to keep the email and max_num_admins validation triggering for this action but not the password one from devise ?

1

There are 1 best solutions below

0
Lam Phan On

the validations chain is a chain of callbacks and the result of validations chain is the errors after all (return true if errors.empty?), there's no way to filter (or fail early), so when your model setup devise, all devise's validations is add to the validations chain, no way to filter them out but remove devise. So i came up with the idea that you should define a special validation scope that will append at the end of validations chain and will force errors.clear (so that it'll ignore previous devise validations), take a look at below code:

class User < ActiveRecord::Base
 devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable
    
 validate :max_num_admins, on: [:create, :special] # should be the last validation below devise validations

 def max_num_admins
   if self.validation_context == :special
     errors.clear # that mean previous validations (not only devise) be ignored
   end
     
   self.errors.add(:base,
     I18n.t(
      'activerecord.errors.models.administrator.max_reached'
     )
   ) if Administrator.count > 3
 end
end

then

def create
  # ...
  if resource.valid?(:special) # validate at special scope
  end
end

if resource.valid? # this will work as normal

Update

no need to touch any existed validation, you can add at the end of validations chain a validation special that will slice! those errors you want to check:

class User
 # ...
 validate :max_num_admins, on: [:create, :special]
 validate :special, on: [:special]

 def max_num_admins  
   self.errors.add(:max_num_admins,
     I18n.t(
      'activerecord.errors.models.administrator.max_reached'
     )
   ) if Administrator.count > 3
 end

 private
 def special
   # removes all errors except the given keys. 
   # so if you want to check email and max_num_admins
   errors.slice!(
    :email, 
    :max_num_admins
   )
 end
end

then

if resource.valid?(:special) # validate email and max_num_admins
if resource.valid? # check as normal password, email, ..., max_num_admins

note DEPRECATION WARNING: ActiveModel::Errors#slice! is deprecated and will be removed in Rails 6.2.