ruby-on-railsrubyruby-on-rails-4devisedevise-invitable

Rails: devise_invitable, form validation errors: undefined method 'errors' for false:FalseClass


I use Devise and the devise_invitable gem to manage and invite users / members to my Rails app. I have some issues with form validations. I have two fields in my invitation form, one for name and the other for recipient_email. Name is optional. When I click submit button - either when the form is valid or not, e.g. when both fields are empty - I get the following error:

undefined method `errors' for false:FalseClass

The error point to my invitations_controller create action. This method looks like this:

def create

  @user = User.find_by(email: invite_params[:email])
  @team = Team.find_by(id: params[:team_id])
  already_invited = false
  # Check if the user already s invited to the team.
  @team.invitations.each do |invitation|
    if @user && invitation.recipient_email.eql?(@user.email)
      already_invited = true
    end
  end

  if already_invited
    redirect_to edit_team_path(@team), notice: "User already invited"
  else
    # The app crashes here
    super
  end
end

I have also customised these two devise_invitable methods: after_invite_path_for and invite_resource(&block). Much based on this tutorial. They look like this:

def after_invite_path_for(resource)
  team = Team.find_by_id(params[:team_id])
  edit_team_path(team)
end

def invite_resource(&block)
  @user = User.find_by(email: invite_params[:email])
  @team = Team.find_by(id: params[:team_id])

  if @user && @user.email != current_user.email
    token = Digest::SHA1.hexdigest([Time.now, rand].join)
    @user.invite_existing_user!(@user, current_user, token)
    member = @team.members.find_by(user_id: current_or_guest_user.id)
    invitation = @team.invitations.build(invitation_token: token, member_id: member.id, recipient_email: invite_params[:email])
    invitation.save
    @user
  else
    @user = resource_class.invite!(invite_params, current_inviter, &block)
    member = @team.members.find_by(user_id: current_or_guest_user.id)
    invitation = @team.invitations.build(invitation_token: @user.raw_invitation_token, member_id: member.id, recipient_email: invite_params[:email])
    invitation.save
  end
end

I have four relevant models, User (the entity that is getting invited), Team each user can be a Member (join table) of multiple Team. I also have a custom Invitation model, each Team can have many invitations, I use this to list invitations sent for a specific team. The invitation functionality worked before, but I had some issues when the form was invalid, for example when both fields where empty. To fix this I tried to enable this in Devise.rb:

config.validate_on_invite = true 

And in my User model have the following:

devise [...], :invitable, validate_on_invite: true 

In my user model I also have this:

validates_presence_of :name
validates_presence_of :email

And in my custom Invitation model I have this:

validates_presence_of :recipient_email

The issue I had before was that invite_resource was getting called despite invalid input, and I would create a new custom Invitation resource for my team.

What I want in error cases is simply:

  1. Don't create or send invitation
  2. Show error message

How can I get this to work when using devise_invitable?


Solution

  • You're returning false from your custom invite_resource implementation. The base controller calls this method and expects an ActiveRecord model instance back, then examines errors on that. But you're returning false, so it tries to execute false.errors - hence the problem you see when you invoke super.

    See line 27 via:

    https://github.com/scambra/devise_invitable/blob/master/app/controllers/devise/invitations_controller.rb#L25

    ...and the default implementation of invite_resource at:

    https://github.com/scambra/devise_invitable/blob/master/app/controllers/devise/invitations_controller.rb#L81

    ...versus your complex implementation:

    def invite_resource(&block)
      @user = User.find_by(email: invite_params[:email])
      @team = Team.find_by(id: params[:team_id])
    
      if @user && @user.email != current_user.email
        token = Digest::SHA1.hexdigest([Time.now, rand].join)
        @user.invite_existing_user!(@user, current_user, token)
        member = @team.members.find_by(user_id: current_or_guest_user.id)
        invitation = @team.invitations.build(invitation_token: token, member_id: member.id, recipient_email: invite_params[:email])
        invitation.save
        @user
      else
        @user = resource_class.invite!(invite_params, current_inviter, &block)
        member = @team.members.find_by(user_id: current_or_guest_user.id)
        invitation = @team.invitations.build(invitation_token: @user.raw_invitation_token, member_id: member.id, recipient_email: invite_params[:email])
        invitation.save
      end
    end
    

    Note that the else case in the above evaluates to the result of invitation.save, which for validation errors will be false. You forgot the @user declaration as present in the above-else case. Just DRY up that code a bit, e.g. as follows (stylistically this might not be to everyone's taste):

    def invite_resource(&block)
      @user = User.find_by(email: invite_params[:email])
      @team = Team.find_by(id: params[:team_id])
    
      invitation = if @user && @user.email != current_user.email
        token = Digest::SHA1.hexdigest([Time.now, rand].join)
        @user.invite_existing_user!(@user, current_user, token)
        member = @team.members.find_by(user_id: current_or_guest_user.id)
        @team.invitations.build(invitation_token: token, member_id: member.id, recipient_email: invite_params[:email])
      else
        @user = resource_class.invite!(invite_params, current_inviter, &block)
        member = @team.members.find_by(user_id: current_or_guest_user.id)
        @team.invitations.build(invitation_token: @user.raw_invitation_token, member_id: member.id, recipient_email: invite_params[:email])
      end
    
      invitation.save
      @user
    end
    

    That should at least fix your undefined method exception, though it leaves any validation errors on invitation ignored (is a save! in order here, with the whole thing wrapped in User.transaction do... to keep all those invitations atomic?).