How to get Devise's current_user in ActiveRecord callback in Rails?
Asked Answered
F

4

10

I'm using Devise and Rails 3.2.16. I want to automatically insert who created a record and who updated a record. So I have something like this in models:

before_create :insert_created_by
before_update :insert_updated_by

private
def insert_created_by
  self.created_by_id = current_user.id
end
def insert_updated_by
  self.updated_by_id = current_user.id
end

Problem is that I get the error undefined local variable or method 'current_user' because current_user is not visible in a callback. How can I automatically insert who created and updated this record?

If there's an easy way to do it in Rails 4.x I'll make the migration.

Fresco answered 2/1, 2014 at 10:46 Comment(5)
Please see: github.com/plataformatec/devise/issues/601Mascagni
@vee, is there another mechanism I can use to automatically update the created_by_id and updated_by_id attributes?Fresco
I had done something similar to what HarsHarl suggested i.e. Thread.current. If you are willing to go that route, I can come up with an answer.Mascagni
@vee, sure I'd love to see your suggestionFresco
at.,Delivered as promised :).Mascagni
M
15

Editing @HarsHarl's answer would probably have made more sense since this answer is very much similar.

With the Thread.current[:current_user] approach, you would have to make this call to set the User for every request. You've said that you don't like the idea of setting a variable for every single request that is only used so seldom; you could chose to use skip_before_filter to skip setting the User or instead of placing the before_filter in the ApplicationController set it in the controllers where you need the current_user.

A modular approach would be to move the setting of created_by_id and updated_by_id to a concern and include it in models you need to use.

Auditable module:

# app/models/concerns/auditable.rb

module Auditable
  extend ActiveSupport::Concern

  included do 
    # Assigns created_by_id and updated_by_id upon included Class initialization
    after_initialize :add_created_by_and_updated_by

    # Updates updated_by_id for the current instance
    after_save :update_updated_by
  end

  private

  def add_created_by_and_updated_by
    self.created_by_id ||= User.current.id if User.current
    self.updated_by_id ||= User.current.id if User.current
  end

  # Updates current instance's updated_by_id if current_user is not nil and is not destroyed.
  def update_updated_by
    self.updated_by_id = User.current.id if User.current and not destroyed?
  end
end

User Model:

#app/models/user.rb
class User < ActiveRecord::Base
  ...

  def self.current=(user)
    Thread.current[:current_user] = user
  end

  def self.current
    Thread.current[:current_user]
  end
  ...
end

Application Controller:

#app/controllers/application_controller

class ApplicationController < ActionController::Base
  ...
  before_filter :authenticate_user!, :set_current_user

  private

  def set_current_user
    User.current = current_user
  end
end

Example Usage: Include auditable module in one of the models:

# app/models/foo.rb
class Foo < ActiveRecord::Base
  include Auditable
  ...
end

Including Auditable concern in Foo model will assign created_by_id and updated_by_id to Foo's instance upon initialization so you have these attributes to use right after initialization, and they are persisted into the foos table on an after_save callback.

Mascagni answered 4/1, 2014 at 3:49 Comment(4)
after_save callbacks are called when destroying an object?Fresco
@Fresco I went over my answer twice and do not see anywhere where I'd imply that after_save callbacks are called when an object is destroyed. after_save should only be called when an object is created or updated.Mascagni
in your auditable.rb listing, 3rd to last line at the end you wrote: ` and not destroyed?. That would always be true since the update_updated_by` callback is not called when destroyed. Correct?Fresco
@at., oh, that is to handle the case where an instance it deleted using foo.delete. Although it's deleted from the database the instance is still available; it's frozen though but available. If you do foo = Foo.create(...) then foo.delete then foo in your console, you'll see that the deleted instance foo is still available.Mascagni
S
3

another approach is this

class User
class << self
  def current_user=(user)
    Thread.current[:current_user] = user
  end

  def current_user
    Thread.current[:current_user]
  end
end
end

class ApplicationController
  before_filter :set_current_user

  def set_current_user
    User.current_user = current_user
  end
end
Sparling answered 2/1, 2014 at 11:6 Comment(1)
This seems better, but I guess I don't like the idea of setting a variable for every single request that is only used so seldom.Fresco
S
1

current_user is not accessible from within model files in Rails, only controllers, views and helpers. Although , through class variable you can achieve that but this is not good approach so for that you can create two methods inside his model. When create action call from controller then send current user and field name to that model ex:

Contoller code
def create
  your code goes here and after save then write
  @model_instance.insert_created_by(current_user)
end

and in model write this method

def self.insert_created_by(user)
  update_attributes(created_by_id: user.id)
end

same for other methods

Sparling answered 2/1, 2014 at 11:4 Comment(4)
It is sad because by doing this, you write the record two times instead of once...Sounder
I'm not sure I understand this. It doesn't automatically set the create_by_id. I might as well simply add that attribute in the create method, no need for a class method.Fresco
yes i am agree, you can add it to simply creation time no need for create separate method.. but i am giving just reply according to your ques. Also in your above code there is no need of call back if you use this approach.Sparling
this is wrong def self.insert_created_by(user) update_attributes(created_by_id: user.id) end map the class not the instance of class so which record will be updated by that. think againDeoxidize
P
0

just create an attribute accessor in the model and initialize it when your record is being saved in controller as below

# app/models/foo.rb
class Foo < ActiveRecord::Base
  attr_accessor :current_user
  before_create :insert_created_by
  before_update :insert_updated_by

  private
   def insert_created_by
    self.created_by_id = current_user.id
  end
  def insert_updated_by
   self.updated_by_id = current_user.id
  end
end
# app/controllers/foos_controller.rb
class FoosController < ApplicationController
 def create
  @foo = Foo.new(....)
  @foo.current_user = current_user
  @foo.save
 end
end
Pindaric answered 2/12, 2022 at 1:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.