ruby-on-railsdevisedevise-invitable

Devise Invitable is modifying existing users


CONTEXT

I'm using devise_invitable to allow a user (with an admin role) to register another user in my app.

User objects have an email, password, token (random string), role (also string) and an associated HealthRecord object which has name, last name, dni (personal id) plus some extra info

PROBLEM

For some reason, when I input an existing email, I get an error (which is intended validation) but it also destroys the HealthRecord associated with the user who has that existing email.

CODE

This is what my console shows upon trying to create the user with existing email

Started POST "/users/invitation" for ::1 at 2021-11-26 10:04:15 -0300
Processing by Users::InvitationsController#create as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"email"=>"paciente1@example.com", "role"=>"Paciente", "health_record_attributes"=>{"residencia"=>"Cementerio", "nombre"=>"overriding", "apellido"=>"test", "dni"=>"123456789", "risk"=>"0", "birth"=>"1999-02-12"}}, "commit"=>"Registrar"}
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 5], ["LIMIT", 1]]
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? ORDER BY "users"."id" ASC LIMIT ?  [["email", "paciente1@example.com"], ["LIMIT", 1]]
  HealthRecord Load (0.1ms)  SELECT "health_records".* FROM "health_records" WHERE "health_records"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  HealthRecord Destroy (0.5ms)  DELETE FROM "health_records" WHERE "health_records"."id" = ?  [["id", 1]]
  TRANSACTION (207.2ms)  commit transaction
  User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "paciente1@example.com"], ["id", 1], ["LIMIT", 1]]
  HealthRecord Exists? (0.4ms)  SELECT 1 AS one FROM "health_records" WHERE "health_records"."dni" = ? LIMIT ?  [["dni", "123456789"], ["LIMIT", 1]]
  Rendering layout layouts/application.html.erb
  Rendering users/invitations/new.html.erb within layouts/application
  HealthRecord Load (0.1ms)  SELECT "health_records".* FROM "health_records" WHERE "health_records"."user_id" = ? LIMIT ?  [["user_id", 5], ["LIMIT", 1]]
  ↳ app/views/users/invitations/new.html.erb:18

The view to generate the new user

<h2>Registro excepcional</h2>

<%= form_for(setup_user(resource), as: resource_name, url: invitation_path(resource_name), html: { method: :post }) do |f| %>

  <% resource.class.invite_key_fields.each do |field| -%>
    <div class="field">
      <%= f.label field %><br />
      <%= f.text_field field, class: 'form-control'%> 
    </div>
  <% end %>

  <div class="field">
    <%= f.hidden_field :role, :value=>"Paciente"%> 
  </div>

  <%= f.fields_for :health_record do |ff| %>
    <div class="field">
      <%= ff.hidden_field :residencia, :value=>current_user.health_record.residencia%> 
    </div>
    
    <div class="field">
      <%= ff.label "Nombre" %><br/>
      <%= ff.text_field :nombre, class: 'form-control',:required => true%> 
    </div>
    <div class="field">
      <%= ff.label "Apellido" %><br/>
      <%= ff.text_field :apellido, class: 'form-control',:required => true%> 
    </div>
    <div class="field">
      <%= ff.label "DNI" %><br/>
      <%= ff.text_field :dni, class: 'form-control',:required => true%> 
    </div>
    <div class="field">
      <%= ff.label "Es de riesgo:",:required => true %>
      <%= ff.check_box :risk  %> 
    </div>
    <div class="field">
        <%= ff.label "Fecha de nacimiento:"%><br/>
        <%= ff.date_field :birth, class: 'form-control',:required => true%> 
    </div>
  <% end %>
  <br/>
  <div class="actions">
    <%= f.submit "Registrar" %>
  </div>
<% end %>

The user model which has validate_on_invite

class User < ApplicationRecord
  devise :invitable, :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :authentication_keys => [:token,:email], :validate_on_invite => true
  has_many :comprobantes, :dependent => :destroy
  has_one :health_record, :dependent => :destroy
  has_many :TurnoAsignado, :dependent => :destroy 
  has_many :TurnoNoAsignado, :dependent => :destroy

  validates :email, uniqueness: true
  
  before_save :init

  accepts_nested_attributes_for :health_record

  def init()
    if self.token.nil?
      self.token = (rand()*1000000).to_i
    end
  end

end

The HealthRecord model

class HealthRecord < ApplicationRecord
  belongs_to :user
  validates :dni, presence: true
  validates :dni, uniqueness: true
  validates :nombre, presence: true
  validates :apellido, presence: true
  validates :birth, presence: true
  before_save :upcase_content
  
  def upcase_content
      self.nombre=self.nombre.downcase
      self.apellido=self.apellido.downcase
      self.nombre=self.nombre.split(/ |\_/).map(&:capitalize).join(" ")
      self.apellido=self.apellido.split(/ |\_/).map(&:capitalize).join(" ")
  end

end

The invitation controller (it's pretty much default I just added parameters and an after_path)

class Users::InvitationsController < Devise::InvitationsController
   
  before_action :configure_permitted_parameters

    #Permit the new params here.
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:invite, keys: [
        :token,
        :role,
        health_record_attributes: [
            :apellido, 
            :nombre,
            :dni,
            :risk, 
            :birth,
            :residencia
          ]
        ])
  end

  def after_invite_path_for(resource)
    new_asignado_path(self.resource.id)
  end
  
end

Solution

  • I think the problem may have been this line

    form_for(setup_user(resource),...
    

    I am using a helper to set the HealthRecord of a user to an empty one (fields_for needed the user to have a HealthRecord to work)

    module FormHelper
        def setup_user(user)
          user.health_record ||= HealthRecord.new # ||= means “assign this value unless it already has a value”
          user
        end
      end
    

    Maybe what was happening is that the empty HealthRecord was assigned to the existing user, somehow?

    I solved it by intercepting the flow of the create method in the invitations controller, by asking if the user email or dni exists

    def create
        @correo = User.find_by(email:params[:user][:email])
        @dni = HealthRecord.find_by(dni:params[:user][:health_record_attributes][:dni])
        
        if (@correo.nil? && @dni.nil?) #si no existe mail ni dni
          super
        else
          mensaje="Los siguientes campos ya estan registrados:"
          if !(@correo.nil?)
            mensaje = mensaje + " email"
          end
          if !(@dni.nil?)
            mensaje = mensaje + " dni"  
          end
          flash[:notice] = mensaje
          redirect_to new_user_invitation_path
        end
      end
    

    Although this works, I'm not sure about the reason of the problem, any insight is welcome